// 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 // counts as the last unlocker when it is removed by its directory name. //nolint:testpackage // white-box test of unexported internals package cli import ( "path/filepath" "strings" "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 // counts as the vault's last one, since the corrupt unlocker cannot unlock // the vault, and that the corrupt one, removed by its directory name, does // not. Either is removed once the user confirms. func TestUnlockerRemoveWithCorruptUnlocker(t *testing.T) { t.Parallel() tests := []struct { name string unlockerID string wantLast bool wantEntries []string }{ { name: "the other unlocker", unlockerID: "pgp-" + listTestGPGKeyID + "B", wantLast: true, wantEntries: []string{listTestUnlockerDirOne}, }, { name: "the corrupt unlocker by its directory name", unlockerID: listTestUnlockerDirOne, wantEntries: []string{listTestUnlockerDirTwo}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() fs := newCorruptUnlockerVault(t) writeTestSecret(t, fs, testVaultDir(listTestVaultName)) instance, cmd := newTestInstance(fs) found, err := instance.findUnlockerToRemove(tt.unlockerID) require.NoError(t, err) assert.Equal(t, tt.wantLast, found.last) instance.terminal = strings.NewReader("y\n") require.NoError(t, instance.UnlockersRemove(tt.unlockerID, false, cmd)) assertDirEntries(t, fs, filepath.Join(testVaultDir(listTestVaultName), listTestUnlockersDirName), tt.wantEntries...) }) } } // TestUnlockerRemoveWithUnreadableMetadata asserts that the only unlocker // of a vault with secrets, removed by its directory name when its metadata // file cannot be checked for or read, counts as the vault's last unlocker, // so the question warns that it is: listing leaves it out, but it may // still be the vault's only working unlocker. It is then removed. The // state directory lock refuses the failing filesystem, so the test calls // findUnlockerToRemove and removeUnlocker, which UnlockersRemove runs to // make its checks and, once it holds the lock, to remove the unlocker. 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)) found, err := instance.findUnlockerToRemove(listTestUnlockerDirOne) require.NoError(t, err) assert.True(t, found.last) assert.Contains(t, found.question, "the last unlocker") require.NoError(t, instance.removeUnlocker(listTestUnlockerDirOne, found, cmd)) assertDirEntries(t, base, unlockersDir) }) } }