package plugins import ( "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const testCommit = "0123456789abcdef0123456789abcdef01234567" func TestPluginLockfileRoundTrip(t *testing.T) { fs := afero.NewMemMapFs() store := NewLockStore(fs, "/config/plugins.lock.json") lock := NewPluginLockfile() lock.Plugins["weather"] = LockedPlugin{ Repo: "https://github.com/example/plugins.git", Path: "plugins/weather", Commit: testCommit, } require.NoError(t, store.Write(lock)) loaded, err := store.Load() require.NoError(t, err) assert.Equal(t, lock, loaded) data, err := afero.ReadFile(fs, store.Path()) require.NoError(t, err) assert.Equal(t, "{\n \"lockfileVersion\": 1,\n \"plugins\": {\n \"weather\": {\n \"repo\": \"https://github.com/example/plugins.git\",\n \"path\": \"plugins/weather\",\n \"commit\": \"0123456789abcdef0123456789abcdef01234567\"\n }\n }\n}\n", string(data)) } func TestPluginLockfileValidation(t *testing.T) { tests := []struct { name string lock PluginLockfile want string }{ { name: "rejects unsupported version", lock: PluginLockfile{LockfileVersion: 2, Plugins: map[string]LockedPlugin{}}, want: "unsupported plugin lockfile version", }, { name: "rejects unsafe id", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{"../bad": {Repo: "repo", Commit: testCommit}}}, want: "invalid locked plugin id", }, { name: "rejects unsafe repository path", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{"bad": {Repo: "repo", Path: "../bad", Commit: testCommit}}}, want: "invalid repository path", }, { name: "rejects abbreviated commit", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{"bad": {Repo: "repo", Commit: "abc123"}}}, want: "invalid commit", }, { name: "rejects repository credentials", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{"bad": {Repo: "https://token@example.com/plugin.git", Commit: testCommit}}}, want: "must not contain credentials", }, { name: "rejects ssh password", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{"bad": {Repo: "ssh://git:secret@example.com/plugin.git", Commit: testCommit}}}, want: "must not contain credentials", }, { name: "rejects conflicting monorepo commits", lock: PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{ "one": {Repo: "repo", Commit: testCommit}, "two": {Repo: "repo", Commit: "1123456789abcdef0123456789abcdef01234567"}, }}, want: "must use the same commit", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.lock.Validate() require.Error(t, err) assert.Contains(t, err.Error(), tt.want) }) } } func TestPluginLockfileAcceptsSSHRemotes(t *testing.T) { repos := []string{ "https://github.com/user/plugin.git", "ssh://git@github.com/user/plugin.git", "git@github.com:user/plugin.git", } for _, repo := range repos { t.Run(repo, func(t *testing.T) { lock := PluginLockfile{LockfileVersion: 1, Plugins: map[string]LockedPlugin{ "plugin": {Repo: repo, Commit: testCommit}, }} assert.NoError(t, lock.Validate()) }) } } func TestMissingPluginLockfileIsEmpty(t *testing.T) { store := NewLockStore(afero.NewMemMapFs(), "/config/plugins.lock.json") lock, err := store.Load() require.NoError(t, err) assert.Equal(t, pluginLockfileVersion, lock.LockfileVersion) assert.Empty(t, lock.Plugins) }