check / check (push) Failing after 1s
findUnlockerByID failed on the first unlocker directory whose metadata could not be checked, read or parsed, so `secret unlocker select` and `remove` failed when one sorted before the unlocker asked for. It now skips such a directory with the warning ListUnlockers gives, through the code both now share. A skipped directory is removed by its directory name, with RemoveDirAtomic, and cannot be selected. Removing one whose metadata file is missing or corrupt never counts as removing the last unlocker; removing one whose metadata file cannot be checked for or read does, since it may be the only working unlocker. Model: opus-5-5
168 lines
5.1 KiB
Go
168 lines
5.1 KiB
Go
// 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)
|
|
})
|
|
}
|
|
}
|