mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-07 05:58:28 -04:00
3f6cd0b579
feat(registry): support multiple plugin/theme registries closes #2763 --------- Co-authored-by: bbedward <bbedward@gmail.com>
502 lines
14 KiB
Go
502 lines
14 KiB
Go
package plugins
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testRegistryURL = "https://example.com/test-registry.git"
|
|
|
|
type mockGitClient struct {
|
|
cloneFunc func(path string, url string) error
|
|
pullFunc func(path string) error
|
|
originFunc func(path string) (string, error)
|
|
hasUpdatesFunc func(path string) (bool, string, string, error)
|
|
}
|
|
|
|
func (m *mockGitClient) PlainClone(path string, url string) error {
|
|
if m.cloneFunc != nil {
|
|
return m.cloneFunc(path, url)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockGitClient) Pull(path string) error {
|
|
if m.pullFunc != nil {
|
|
return m.pullFunc(path)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockGitClient) OriginURL(path string) (string, error) {
|
|
if m.originFunc != nil {
|
|
return m.originFunc(path)
|
|
}
|
|
return "", errors.New("not a repository")
|
|
}
|
|
|
|
func (m *mockGitClient) HasUpdates(path string) (bool, string, string, error) {
|
|
if m.hasUpdatesFunc != nil {
|
|
return m.hasUpdatesFunc(path)
|
|
}
|
|
return false, "", "", nil
|
|
}
|
|
|
|
func TestNewRegistry(t *testing.T) {
|
|
registry, err := NewRegistry()
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, registry)
|
|
assert.NotEmpty(t, registry.cacheDir)
|
|
require.NotEmpty(t, registry.registries)
|
|
assert.Equal(t, registries.OfficialName, registry.registries[0].Name)
|
|
}
|
|
|
|
func TestGetCacheDir(t *testing.T) {
|
|
cacheDir := getCacheDir()
|
|
assert.Contains(t, cacheDir, "/tmp/dankdots-plugin-registry")
|
|
}
|
|
|
|
func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
|
|
fs := afero.NewMemMapFs()
|
|
tmpDir := "/test-cache"
|
|
registry := &Registry{
|
|
fs: fs,
|
|
cacheDir: tmpDir,
|
|
registries: []registries.Source{{Name: "test", URL: testRegistryURL}},
|
|
plugins: []Plugin{},
|
|
git: &mockGitClient{},
|
|
}
|
|
return registry, fs, tmpDir
|
|
}
|
|
|
|
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
|
pluginsDir := filepath.Join(dir, "plugins")
|
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
data, err := json.Marshal(plugin)
|
|
require.NoError(t, err)
|
|
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestLoadPlugins(t *testing.T) {
|
|
t.Run("loads valid plugin files", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
plugin1 := Plugin{
|
|
Name: "TestPlugin1",
|
|
Capabilities: []string{"dankbar-widget"},
|
|
Category: "monitoring",
|
|
Repo: "https://github.com/test/plugin1",
|
|
Author: "Test Author",
|
|
Description: "Test plugin 1",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
|
|
plugin2 := Plugin{
|
|
Name: "TestPlugin2",
|
|
Capabilities: []string{"system-tray"},
|
|
Category: "utilities",
|
|
Repo: "https://github.com/test/plugin2",
|
|
Author: "Another Author",
|
|
Description: "Test plugin 2",
|
|
Dependencies: []string{"dep1", "dep2"},
|
|
Compositors: []string{"hyprland", "niri"},
|
|
Distro: []string{"arch"},
|
|
Screenshot: "https://example.com/screenshot.png",
|
|
}
|
|
|
|
createTestPlugin(t, fs, tmpDir, "plugin1.json", plugin1)
|
|
createTestPlugin(t, fs, tmpDir, "plugin2.json", plugin2)
|
|
|
|
plugins, err := registry.loadPluginsFrom(tmpDir)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 2)
|
|
|
|
assert.Equal(t, "TestPlugin1", plugins[0].Name)
|
|
assert.Equal(t, "TestPlugin2", plugins[1].Name)
|
|
assert.Equal(t, []string{"dankbar-widget"}, plugins[0].Capabilities)
|
|
assert.Equal(t, []string{"dep1", "dep2"}, plugins[1].Dependencies)
|
|
})
|
|
|
|
t.Run("skips non-json files", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
plugin := Plugin{
|
|
Name: "ValidPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
createTestPlugin(t, fs, tmpDir, "valid.json", plugin)
|
|
|
|
plugins, err := registry.loadPluginsFrom(tmpDir)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 1)
|
|
assert.Equal(t, "ValidPlugin", plugins[0].Name)
|
|
})
|
|
|
|
t.Run("skips invalid json files", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
pluginsDir := filepath.Join(tmpDir, "plugins")
|
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
plugin := Plugin{
|
|
Name: "ValidPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
createTestPlugin(t, fs, tmpDir, "valid.json", plugin)
|
|
|
|
plugins, err := registry.loadPluginsFrom(tmpDir)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 1)
|
|
assert.Equal(t, "ValidPlugin", plugins[0].Name)
|
|
})
|
|
|
|
t.Run("missing plugins directory is a themes-only registry", func(t *testing.T) {
|
|
registry, _, _ := setupTestRegistry(t)
|
|
|
|
plugins, err := registry.loadPluginsFrom(registry.cacheDir)
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, plugins)
|
|
})
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
t.Run("returns cached plugins if available", func(t *testing.T) {
|
|
registry, _, _ := setupTestRegistry(t)
|
|
|
|
plugin := Plugin{
|
|
Name: "CachedPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
|
|
registry.plugins = []Plugin{plugin}
|
|
|
|
plugins, err := registry.List()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 1)
|
|
assert.Equal(t, "CachedPlugin", plugins[0].Name)
|
|
})
|
|
|
|
t.Run("updates and loads plugins when cache is empty", func(t *testing.T) {
|
|
registry, fs, _ := setupTestRegistry(t)
|
|
|
|
plugin := Plugin{
|
|
Name: "NewPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
plugins, err := registry.List()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 1)
|
|
assert.Equal(t, "NewPlugin", plugins[0].Name)
|
|
})
|
|
|
|
t.Run("partial registry failure still returns loaded plugins", func(t *testing.T) {
|
|
registry, fs, _ := setupTestRegistry(t)
|
|
|
|
registry.registries = []registries.Source{
|
|
{Name: "test", URL: testRegistryURL},
|
|
{Name: "broken", URL: "https://example.com/broken.git"},
|
|
}
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
if url != testRegistryURL {
|
|
return errors.New("clone failed")
|
|
}
|
|
createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
plugins, err := registry.List()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, plugins, 1)
|
|
})
|
|
}
|
|
|
|
func TestUpdate(t *testing.T) {
|
|
t.Run("clones repository when cache doesn't exist", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
plugin := Plugin{
|
|
Name: "RepoPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
|
|
cloneCalled := false
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
cloneCalled = true
|
|
assert.Equal(t, testRegistryURL, url)
|
|
assert.Equal(t, filepath.Join(tmpDir, "test"), path)
|
|
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.True(t, cloneCalled)
|
|
assert.Len(t, registry.plugins, 1)
|
|
assert.Equal(t, "RepoPlugin", registry.plugins[0].Name)
|
|
})
|
|
|
|
t.Run("pulls when cache exists with matching origin", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
plugin := Plugin{
|
|
Name: "UpdatedPlugin",
|
|
Capabilities: []string{"test"},
|
|
Category: "test",
|
|
Repo: "https://github.com/test/test",
|
|
Author: "Test",
|
|
Description: "Test",
|
|
Compositors: []string{"niri"},
|
|
Distro: []string{"any"},
|
|
}
|
|
|
|
subdir := filepath.Join(tmpDir, "test")
|
|
require.NoError(t, fs.MkdirAll(subdir, 0o755))
|
|
|
|
pullCalled := false
|
|
registry.git = &mockGitClient{
|
|
originFunc: func(path string) (string, error) {
|
|
return testRegistryURL, nil
|
|
},
|
|
pullFunc: func(path string) error {
|
|
pullCalled = true
|
|
assert.Equal(t, subdir, path)
|
|
createTestPlugin(t, fs, path, "plugin.json", plugin)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.True(t, pullCalled)
|
|
assert.Len(t, registry.plugins, 1)
|
|
assert.Equal(t, "UpdatedPlugin", registry.plugins[0].Name)
|
|
})
|
|
|
|
t.Run("re-clones when cached origin does not match configured URL", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
subdir := filepath.Join(tmpDir, "test")
|
|
require.NoError(t, fs.MkdirAll(subdir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(subdir, "stale"), []byte("x"), 0o644))
|
|
|
|
pullCalled := false
|
|
cloneCalled := false
|
|
registry.git = &mockGitClient{
|
|
originFunc: func(path string) (string, error) {
|
|
return "https://example.com/old-origin.git", nil
|
|
},
|
|
pullFunc: func(path string) error {
|
|
pullCalled = true
|
|
return nil
|
|
},
|
|
cloneFunc: func(path string, url string) error {
|
|
cloneCalled = true
|
|
assert.Equal(t, testRegistryURL, url)
|
|
createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.False(t, pullCalled, "stale origin must not be pulled")
|
|
assert.True(t, cloneCalled)
|
|
exists, _ := afero.Exists(fs, filepath.Join(subdir, "stale"))
|
|
assert.False(t, exists, "stale cache contents removed before re-clone")
|
|
})
|
|
|
|
t.Run("re-clones when pull fails", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
subdir := filepath.Join(tmpDir, "test")
|
|
require.NoError(t, fs.MkdirAll(subdir, 0o755))
|
|
|
|
cloneCalled := false
|
|
registry.git = &mockGitClient{
|
|
originFunc: func(path string) (string, error) {
|
|
return testRegistryURL, nil
|
|
},
|
|
pullFunc: func(path string) error {
|
|
return errors.New("shallow clone corruption")
|
|
},
|
|
cloneFunc: func(path string, url string) error {
|
|
cloneCalled = true
|
|
createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.True(t, cloneCalled)
|
|
})
|
|
|
|
t.Run("aggregates from multiple registries", func(t *testing.T) {
|
|
registry, fs, _ := setupTestRegistry(t)
|
|
|
|
pluginA := Plugin{ID: "a", Name: "PluginA", Compositors: []string{"niri"}, Distro: []string{"any"}}
|
|
pluginB := Plugin{ID: "b", Name: "PluginB", Compositors: []string{"niri"}, Distro: []string{"any"}}
|
|
|
|
registry.registries = []registries.Source{
|
|
{Name: "official", URL: testRegistryURL},
|
|
{Name: "louzt", URL: "https://example.com/louzt.git"},
|
|
}
|
|
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
switch filepath.Base(path) {
|
|
case "official":
|
|
createTestPlugin(t, fs, path, "x.json", pluginA)
|
|
case "louzt":
|
|
createTestPlugin(t, fs, path, "x.json", pluginB)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, registry.plugins, 2)
|
|
assert.Equal(t, "a", registry.plugins[0].ID)
|
|
assert.Equal(t, "b", registry.plugins[1].ID)
|
|
})
|
|
|
|
t.Run("dedupes by ID with declaration order priority", func(t *testing.T) {
|
|
registry, fs, _ := setupTestRegistry(t)
|
|
|
|
pluginOfficial := Plugin{ID: "weather", Name: "OfficialWeather", Compositors: []string{"niri"}, Distro: []string{"any"}}
|
|
pluginLouzt := Plugin{ID: "weather", Name: "LouztWeather", Compositors: []string{"niri"}, Distro: []string{"any"}}
|
|
|
|
registry.registries = []registries.Source{
|
|
{Name: "official", URL: testRegistryURL},
|
|
{Name: "louzt", URL: "https://example.com/louzt.git"},
|
|
}
|
|
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
switch filepath.Base(path) {
|
|
case "official":
|
|
createTestPlugin(t, fs, path, "x.json", pluginOfficial)
|
|
case "louzt":
|
|
createTestPlugin(t, fs, path, "x.json", pluginLouzt)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, registry.plugins, 1)
|
|
assert.Equal(t, "OfficialWeather", registry.plugins[0].Name)
|
|
})
|
|
|
|
t.Run("continues past failing registry and reports it", func(t *testing.T) {
|
|
registry, fs, _ := setupTestRegistry(t)
|
|
|
|
registry.registries = []registries.Source{
|
|
{Name: "broken", URL: "https://example.com/broken.git"},
|
|
{Name: "test", URL: testRegistryURL},
|
|
}
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
if url != testRegistryURL {
|
|
return errors.New("network unreachable")
|
|
}
|
|
createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "registry broken")
|
|
assert.Len(t, registry.plugins, 1, "healthy registry still loads")
|
|
})
|
|
|
|
t.Run("removes legacy single-clone cache at base", func(t *testing.T) {
|
|
registry, fs, tmpDir := setupTestRegistry(t)
|
|
|
|
require.NoError(t, fs.MkdirAll(filepath.Join(tmpDir, ".git"), 0o755))
|
|
createTestPlugin(t, fs, tmpDir, "legacy.json", Plugin{ID: "legacy", Name: "Legacy"})
|
|
|
|
registry.git = &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
createTestPlugin(t, fs, path, "x.json", Plugin{ID: "x", Name: "X"})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
err := registry.Update()
|
|
assert.NoError(t, err)
|
|
exists, _ := afero.DirExists(fs, filepath.Join(tmpDir, ".git"))
|
|
assert.False(t, exists, "legacy clone removed")
|
|
assert.Len(t, registry.plugins, 1)
|
|
assert.Equal(t, "x", registry.plugins[0].ID)
|
|
})
|
|
}
|