mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-08 14:38:30 -04:00
b7ec57a534
* feat(plugins): add portable plugin lockfile * plugins/lockfile: allow ssh remotes --------- Co-authored-by: bbedward <bbedward@gmail.com>
491 lines
16 KiB
Go
491 lines
16 KiB
Go
package plugins
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupTestManager(t *testing.T) (*Manager, afero.Fs, string) {
|
|
fs := afero.NewMemMapFs()
|
|
pluginsDir := "/test-plugins"
|
|
manager := &Manager{
|
|
fs: fs,
|
|
pluginsDir: pluginsDir,
|
|
lockPath: "/config/plugins.lock.json",
|
|
gitClient: &mockGitClient{},
|
|
}
|
|
return manager, fs, pluginsDir
|
|
}
|
|
|
|
func TestNewManager(t *testing.T) {
|
|
manager, err := NewManager()
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, manager)
|
|
assert.NotEmpty(t, manager.pluginsDir)
|
|
}
|
|
|
|
func TestGetPluginsDir(t *testing.T) {
|
|
t.Run("uses XDG_CONFIG_HOME when set", func(t *testing.T) {
|
|
oldConfig := os.Getenv("XDG_CONFIG_HOME")
|
|
defer func() {
|
|
if oldConfig != "" {
|
|
os.Setenv("XDG_CONFIG_HOME", oldConfig)
|
|
} else {
|
|
os.Unsetenv("XDG_CONFIG_HOME")
|
|
}
|
|
}()
|
|
|
|
os.Setenv("XDG_CONFIG_HOME", "/tmp/test-config")
|
|
dir := getPluginsDir()
|
|
assert.Equal(t, "/tmp/test-config/DankMaterialShell/plugins", dir)
|
|
})
|
|
|
|
t.Run("falls back to home directory", func(t *testing.T) {
|
|
oldConfig := os.Getenv("XDG_CONFIG_HOME")
|
|
defer func() {
|
|
if oldConfig != "" {
|
|
os.Setenv("XDG_CONFIG_HOME", oldConfig)
|
|
} else {
|
|
os.Unsetenv("XDG_CONFIG_HOME")
|
|
}
|
|
}()
|
|
|
|
os.Unsetenv("XDG_CONFIG_HOME")
|
|
dir := getPluginsDir()
|
|
assert.Contains(t, dir, ".config/DankMaterialShell/plugins")
|
|
})
|
|
}
|
|
|
|
func TestIsInstalled(t *testing.T) {
|
|
t.Run("returns true when plugin is installed", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
err := fs.MkdirAll(pluginPath, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
installed, err := manager.IsInstalled(plugin)
|
|
assert.NoError(t, err)
|
|
assert.True(t, installed)
|
|
})
|
|
|
|
t.Run("returns false when plugin is not installed", func(t *testing.T) {
|
|
manager, _, _ := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
|
installed, err := manager.IsInstalled(plugin)
|
|
assert.NoError(t, err)
|
|
assert.False(t, installed)
|
|
})
|
|
}
|
|
|
|
func TestInstall(t *testing.T) {
|
|
t.Run("installs plugin successfully", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
plugin := Plugin{
|
|
ID: "test-plugin",
|
|
Name: "TestPlugin",
|
|
Repo: "https://github.com/test/plugin",
|
|
}
|
|
|
|
cloneCalled := false
|
|
mockGit := &mockGitClient{
|
|
cloneFunc: func(path string, url string) error {
|
|
cloneCalled = true
|
|
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
|
assert.Equal(t, plugin.Repo, url)
|
|
return fs.MkdirAll(path, 0o755)
|
|
},
|
|
}
|
|
manager.gitClient = mockGit
|
|
|
|
err := manager.Install(plugin)
|
|
assert.NoError(t, err)
|
|
assert.True(t, cloneCalled)
|
|
|
|
exists, _ := afero.DirExists(fs, filepath.Join(pluginsDir, plugin.ID))
|
|
assert.True(t, exists)
|
|
})
|
|
|
|
t.Run("returns error when plugin already installed", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
err := fs.MkdirAll(pluginPath, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
err = manager.Install(plugin)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "already installed")
|
|
})
|
|
|
|
t.Run("installs monorepo plugin with symlink", func(t *testing.T) {
|
|
t.Skip("Skipping symlink test as MemMapFs doesn't support symlinks")
|
|
})
|
|
}
|
|
|
|
func TestManagerUpdate(t *testing.T) {
|
|
t.Run("updates plugin successfully", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
err := fs.MkdirAll(pluginPath, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
pullCalled := false
|
|
mockGit := &mockGitClient{
|
|
pullFunc: func(path string) error {
|
|
pullCalled = true
|
|
assert.Equal(t, pluginPath, path)
|
|
return nil
|
|
},
|
|
}
|
|
manager.gitClient = mockGit
|
|
|
|
err = manager.Update(plugin)
|
|
assert.NoError(t, err)
|
|
assert.True(t, pullCalled)
|
|
})
|
|
|
|
t.Run("returns error when plugin not installed", func(t *testing.T) {
|
|
manager, _, _ := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
|
err := manager.Update(plugin)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not installed")
|
|
})
|
|
}
|
|
|
|
func TestUninstall(t *testing.T) {
|
|
t.Run("uninstalls plugin successfully", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
err := fs.MkdirAll(pluginPath, 0o755)
|
|
require.NoError(t, err)
|
|
|
|
err = manager.Uninstall(plugin)
|
|
assert.NoError(t, err)
|
|
|
|
exists, _ := afero.DirExists(fs, pluginPath)
|
|
assert.False(t, exists)
|
|
})
|
|
|
|
t.Run("returns error when plugin not installed", func(t *testing.T) {
|
|
manager, _, _ := setupTestManager(t)
|
|
|
|
plugin := Plugin{ID: "non-existent", Name: "NonExistent"}
|
|
err := manager.Uninstall(plugin)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not installed")
|
|
})
|
|
}
|
|
|
|
func TestListInstalled(t *testing.T) {
|
|
t.Run("lists installed plugins", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
|
require.NoError(t, err)
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0o755)
|
|
require.NoError(t, err)
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
installed, err := manager.ListInstalled()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, installed, 2)
|
|
assert.Contains(t, installed, "Plugin1")
|
|
assert.Contains(t, installed, "Plugin2")
|
|
})
|
|
|
|
t.Run("returns empty list when no plugins installed", func(t *testing.T) {
|
|
manager, _, _ := setupTestManager(t)
|
|
|
|
installed, err := manager.ListInstalled()
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, installed)
|
|
})
|
|
|
|
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
|
|
err := fs.MkdirAll(pluginsDir, 0o755)
|
|
require.NoError(t, err)
|
|
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
|
require.NoError(t, err)
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
|
require.NoError(t, err)
|
|
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0o755)
|
|
require.NoError(t, err)
|
|
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
installed, err := manager.ListInstalled()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, installed, 1)
|
|
assert.Equal(t, "Plugin1", installed[0])
|
|
})
|
|
}
|
|
|
|
func TestManagerGetPluginsDir(t *testing.T) {
|
|
manager, _, pluginsDir := setupTestManager(t)
|
|
assert.Equal(t, pluginsDir, manager.GetPluginsDir())
|
|
}
|
|
|
|
func TestInstallUpdatesLockfile(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
plugin := Plugin{ID: "test-plugin", Name: "Test Plugin", Repo: "https://github.com/test/plugin.git"}
|
|
manager.gitClient = &mockGitClient{
|
|
cloneFunc: func(path, _ string) error {
|
|
require.NoError(t, fs.MkdirAll(path, 0o755))
|
|
return afero.WriteFile(fs, filepath.Join(path, "plugin.json"), []byte(`{"id":"test-plugin"}`), 0o644)
|
|
},
|
|
revisionFunc: func(string) (string, error) { return testCommit, nil },
|
|
}
|
|
|
|
require.NoError(t, manager.Install(plugin))
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, LockedPlugin{Repo: plugin.Repo, Commit: testCommit}, lock.Plugins[plugin.ID])
|
|
exists, err := afero.Exists(fs, filepath.Join(pluginsDir, plugin.ID))
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestUpdateRefreshesLockedCommit(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
plugin := Plugin{ID: "test-plugin", Name: "Test Plugin", Repo: "https://github.com/test/plugin.git"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"test-plugin"}`), 0o644))
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
plugin.ID: {Repo: plugin.Repo, Commit: testCommit},
|
|
},
|
|
}))
|
|
newCommit := "1123456789abcdef0123456789abcdef01234567"
|
|
manager.gitClient = &mockGitClient{
|
|
pullFunc: func(string) error { return nil },
|
|
revisionFunc: func(string) (string, error) { return newCommit, nil },
|
|
}
|
|
|
|
require.NoError(t, manager.Update(plugin))
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, newCommit, lock.Plugins[plugin.ID].Commit)
|
|
}
|
|
|
|
func TestUpdateResolvesRenamedPluginDirectory(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
plugin := Plugin{ID: "test-plugin", Name: "Test Plugin", Repo: "https://github.com/test/plugin.git"}
|
|
pluginPath := filepath.Join(pluginsDir, "renamed-dir")
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"test-plugin"}`), 0o644))
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
plugin.ID: {Repo: plugin.Repo, Commit: testCommit},
|
|
},
|
|
}))
|
|
var pulledPath string
|
|
manager.gitClient = &mockGitClient{
|
|
pullFunc: func(path string) error {
|
|
pulledPath = path
|
|
return nil
|
|
},
|
|
cloneFunc: func(path, _ string) error {
|
|
t.Fatalf("unexpected clone to %s", path)
|
|
return nil
|
|
},
|
|
revisionFunc: func(string) (string, error) { return testCommit, nil },
|
|
}
|
|
|
|
require.NoError(t, manager.Update(plugin))
|
|
assert.Equal(t, pluginPath, pulledPath)
|
|
}
|
|
|
|
func TestUninstallRemovesLockedPlugin(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
plugin := Plugin{ID: "test-plugin", Name: "Test Plugin", Repo: "https://github.com/test/plugin.git"}
|
|
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"test-plugin"}`), 0o644))
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
plugin.ID: {Repo: plugin.Repo, Commit: testCommit},
|
|
},
|
|
}))
|
|
|
|
require.NoError(t, manager.Uninstall(plugin))
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, lock.Plugins, plugin.ID)
|
|
}
|
|
|
|
func TestRestoreFromLockfileInstallsExactCommit(t *testing.T) {
|
|
manager, fs, _ := setupTestManager(t)
|
|
wantedCommit := "2123456789abcdef0123456789abcdef01234567"
|
|
incoming := PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
"restored": {Repo: "https://github.com/test/restored.git", Commit: wantedCommit},
|
|
},
|
|
}
|
|
require.NoError(t, NewLockStore(fs, "/incoming.lock.json").Write(incoming))
|
|
currentCommit := testCommit
|
|
manager.gitClient = &mockGitClient{
|
|
cloneFunc: func(path, _ string) error {
|
|
require.NoError(t, fs.MkdirAll(path, 0o755))
|
|
return afero.WriteFile(fs, filepath.Join(path, "plugin.json"), []byte(`{"id":"restored"}`), 0o644)
|
|
},
|
|
revisionFunc: func(string) (string, error) { return currentCommit, nil },
|
|
checkoutFunc: func(_ string, revision string) error {
|
|
currentCommit = revision
|
|
return nil
|
|
},
|
|
}
|
|
|
|
require.NoError(t, manager.RestoreFromLockfile("/incoming.lock.json", false))
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, wantedCommit, lock.Plugins["restored"].Commit)
|
|
}
|
|
|
|
func TestSnapshotLockfileMigratesLegacyMonorepoMetadata(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
pluginPath := filepath.Join(pluginsDir, "legacy")
|
|
repoPath := filepath.Join(pluginsDir, ".repos", "legacy-repo")
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, fs.MkdirAll(repoPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"legacy"}`), 0o644))
|
|
require.NoError(t, afero.WriteFile(fs, pluginPath+".meta", []byte("repo=https://github.com/test/suite.git\npath=plugins/legacy\nrepodir=legacy-repo"), 0o644))
|
|
manager.gitClient = &mockGitClient{
|
|
revisionFunc: func(path string) (string, error) {
|
|
assert.Equal(t, repoPath, path)
|
|
return testCommit, nil
|
|
},
|
|
}
|
|
|
|
lock, warnings, err := manager.SnapshotLockfile()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, warnings)
|
|
assert.Equal(t, LockedPlugin{
|
|
Repo: "https://github.com/test/suite.git",
|
|
Path: "plugins/legacy",
|
|
Commit: testCommit,
|
|
}, lock.Plugins["legacy"])
|
|
}
|
|
|
|
func TestRecordInstalledPluginUpdatesSharedRepositoryCommit(t *testing.T) {
|
|
manager, _, _ := setupTestManager(t)
|
|
repo := "https://github.com/test/suite.git"
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
"one": {Repo: repo, Path: "plugins/one", Commit: testCommit},
|
|
"two": {Repo: repo, Path: "plugins/two", Commit: testCommit},
|
|
},
|
|
}))
|
|
newCommit := "3123456789abcdef0123456789abcdef01234567"
|
|
manager.gitClient = &mockGitClient{
|
|
revisionFunc: func(string) (string, error) { return newCommit, nil },
|
|
}
|
|
|
|
require.NoError(t, manager.recordInstalledPlugin(Plugin{ID: "one", Repo: repo, Path: "plugins/one"}, "/repo"))
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, newCommit, lock.Plugins["one"].Commit)
|
|
assert.Equal(t, newCommit, lock.Plugins["two"].Commit)
|
|
}
|
|
|
|
func TestRestoreFromLockfilePrunesManagedPlugins(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
pluginPath := filepath.Join(pluginsDir, "old")
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"old","name":"Old"}`), 0o644))
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
"old": {Repo: "https://github.com/test/old.git", Commit: testCommit},
|
|
},
|
|
}))
|
|
require.NoError(t, NewLockStore(fs, "/empty.lock.json").Write(NewPluginLockfile()))
|
|
|
|
require.NoError(t, manager.RestoreFromLockfile("/empty.lock.json", true))
|
|
exists, err := afero.Exists(fs, pluginPath)
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
lock, err := manager.lockStore().Load()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, lock.Plugins)
|
|
}
|
|
|
|
func TestRestoreMissingLockfileDoesNotPrune(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
pluginPath := filepath.Join(pluginsDir, "kept")
|
|
require.NoError(t, fs.MkdirAll(pluginPath, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(pluginPath, "plugin.json"), []byte(`{"id":"kept"}`), 0o644))
|
|
require.NoError(t, manager.lockStore().Write(PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
"kept": {Repo: "https://github.com/test/kept.git", Commit: testCommit},
|
|
},
|
|
}))
|
|
|
|
err := manager.RestoreFromLockfile("/missing.lock.json", true)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not found")
|
|
exists, statErr := afero.Exists(fs, pluginPath)
|
|
require.NoError(t, statErr)
|
|
assert.True(t, exists)
|
|
}
|
|
|
|
func TestRestoreFailedRevisionRemovesPartialInstall(t *testing.T) {
|
|
manager, fs, pluginsDir := setupTestManager(t)
|
|
incoming := PluginLockfile{
|
|
LockfileVersion: 1,
|
|
Plugins: map[string]LockedPlugin{
|
|
"broken": {Repo: "https://github.com/test/broken.git", Commit: testCommit},
|
|
},
|
|
}
|
|
require.NoError(t, NewLockStore(fs, "/incoming.lock.json").Write(incoming))
|
|
manager.gitClient = &mockGitClient{
|
|
cloneFunc: func(path, _ string) error {
|
|
require.NoError(t, fs.MkdirAll(path, 0o755))
|
|
return afero.WriteFile(fs, filepath.Join(path, "plugin.json"), []byte(`{"id":"broken"}`), 0o644)
|
|
},
|
|
revisionFunc: func(string) (string, error) { return testCommit, nil },
|
|
checkoutFunc: func(string, string) error {
|
|
return errors.New("revision unavailable")
|
|
},
|
|
}
|
|
|
|
err := manager.RestoreFromLockfile("/incoming.lock.json", false)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "revision unavailable")
|
|
exists, statErr := afero.Exists(fs, filepath.Join(pluginsDir, "broken"))
|
|
require.NoError(t, statErr)
|
|
assert.False(t, exists)
|
|
lock, loadErr := manager.lockStore().Load()
|
|
require.NoError(t, loadErr)
|
|
assert.NotContains(t, lock.Plugins, "broken")
|
|
}
|