// Corrupt Unlocker Tests // // `secret unlocker select` and `secret unlocker remove` find an unlocker // by its ID. These tests give the first unlocker, which sorts before the // one the commands act on, metadata that is not JSON, and check that the // commands step past it, and that it can itself be removed by its // directory name, which `secret unlocker list` names in its warning. A // last test checks that an unlocker whose metadata file cannot be read is // removed by its directory name only as the last unlocker is. //nolint:testpackage // white-box test of unexported internals package cli import ( "path/filepath" "testing" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // newCorruptUnlockerVault returns the two-unlocker test vault with the // metadata of the first unlocker replaced by text that is not JSON. func newCorruptUnlockerVault(t *testing.T) *afero.MemMapFs { t.Helper() fs := newListTestVault(t, 2) require.NoError(t, afero.WriteFile(fs, filepath.Join(testVaultDir(listTestVaultName), listTestUnlockersDirName, listTestUnlockerDirOne, listTestMetadataFileName), []byte("not json"), listTestFilePerm)) return fs } // TestUnlockerSelectSkipsCorruptUnlocker asserts that the second unlocker // can be selected, and that the corrupt one, having no type to be used as, // cannot be selected by its directory name. func TestUnlockerSelectSkipsCorruptUnlocker(t *testing.T) { t.Parallel() fs := newCorruptUnlockerVault(t) instance, _ := newTestInstance(fs) require.NoError(t, instance.UnlockerSelect("pgp-"+listTestGPGKeyID+"B")) current, err := afero.ReadFile(fs, filepath.Join(testVaultDir(listTestVaultName), "current-unlocker")) require.NoError(t, err) assert.Equal(t, listTestUnlockerDirTwo, string(current)) err = instance.UnlockerSelect(listTestUnlockerDirOne) require.ErrorIs(t, err, vault.ErrUnlockerNotFound) } // TestUnlockerRemoveWithCorruptUnlocker asserts that the second unlocker // can be removed, unless the vault holds secrets: the corrupt unlocker // cannot unlock the vault, so the second is its last. The corrupt one can // be removed by its directory name without --force even then. func TestUnlockerRemoveWithCorruptUnlocker(t *testing.T) { t.Parallel() tests := []struct { name string unlockerID string withSecret bool wantErr error wantEntries []string }{ { name: "the other unlocker", unlockerID: "pgp-" + listTestGPGKeyID + "B", wantEntries: []string{listTestUnlockerDirOne}, }, { name: "the other unlocker, the last one, with secrets", unlockerID: "pgp-" + listTestGPGKeyID + "B", withSecret: true, wantErr: errLastUnlocker, wantEntries: []string{ listTestUnlockerDirOne, listTestUnlockerDirTwo, }, }, { name: "the corrupt unlocker by its directory name", unlockerID: listTestUnlockerDirOne, withSecret: true, wantEntries: []string{listTestUnlockerDirTwo}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() fs := newCorruptUnlockerVault(t) if tt.withSecret { writeTestSecret(t, fs, testVaultDir(listTestVaultName)) } instance, cmd := newTestInstance(fs) err := instance.UnlockersRemove(tt.unlockerID, false, cmd) require.ErrorIs(t, err, tt.wantErr) assertDirEntries(t, fs, filepath.Join(testVaultDir(listTestVaultName), listTestUnlockersDirName), tt.wantEntries...) }) } } // TestUnlockerRemoveWithUnreadableMetadata asserts that removing the only // unlocker of a vault with secrets by its directory name, when its // metadata file cannot be checked for or read, is refused without --force: // listing leaves it out, but it may still be the vault's only working // unlocker. With --force it is removed. The state directory lock refuses // the failing filesystem, so the test calls removeUnlocker, which // UnlockersRemove runs once it holds the lock. func TestUnlockerRemoveWithUnreadableMetadata(t *testing.T) { t.Parallel() vaultDir := testVaultDir(listTestVaultName) unlockersDir := filepath.Join(vaultDir, listTestUnlockersDirName) failingPath := filepath.Join(unlockersDir, listTestUnlockerDirOne, listTestMetadataFileName) tests := []struct { name string wrap func(base afero.Fs) afero.Fs }{ { name: "checking for the file fails", wrap: func(base afero.Fs) afero.Fs { return &metadataStatFailFs{Fs: base, uncheckablePath: failingPath} }, }, { name: "reading the file fails", wrap: func(base afero.Fs) afero.Fs { return &metadataReadFailFs{Fs: base, unreadablePath: failingPath} }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() base := newListTestVault(t, 1) writeTestSecret(t, base, vaultDir) instance, cmd := newTestInstance(tt.wrap(base)) err := instance.removeUnlocker(listTestUnlockerDirOne, false, cmd) require.ErrorIs(t, err, errLastUnlocker) assertDirEntries(t, base, unlockersDir, listTestUnlockerDirOne) require.NoError(t, instance.removeUnlocker(listTestUnlockerDirOne, true, cmd)) assertDirEntries(t, base, unlockersDir) }) } }