check / check (push) Failing after 2s
Keychain and Secure Enclave unlocker IDs were the creation time to the minute plus the host name, and passphrase unlocker IDs the time to the minute, so two created within one minute shared an ID, and `unlocker select`, `unlocker remove` and the selection after `unlocker add` acted on the older one. Every unlocker's ID is now its directory name, unique in its vault. `vault.ListUnlockers` returns each unlocker's metadata keyed by that name, so `unlocker list` and shell completion no longer find IDs by matching metadata. PGP unlocker IDs were `pgp-<fingerprint>`; a second PGP unlocker for one key is refused by comparing fingerprints in metadata. Model: opus-5-5
192 lines
6.2 KiB
Go
192 lines
6.2 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, as can
|
|
// one with no metadata file. 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(listTestUnlockerDirTwo))
|
|
|
|
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: listTestUnlockerDirTwo,
|
|
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...)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUnlockerRemoveWithoutMetadata asserts that a partial unlocker
|
|
// directory, one with no metadata file, removed by its directory name from
|
|
// a vault with secrets, does not count as the vault's last unlocker, since
|
|
// it cannot unlock the vault, so the question says it is not. It is
|
|
// removed once the user confirms.
|
|
func TestUnlockerRemoveWithoutMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := newListTestVault(t, 2)
|
|
vaultDir := testVaultDir(listTestVaultName)
|
|
unlockersDir := filepath.Join(vaultDir, listTestUnlockersDirName)
|
|
|
|
require.NoError(t, fs.Remove(filepath.Join(
|
|
unlockersDir, listTestUnlockerDirOne, listTestMetadataFileName)))
|
|
writeTestSecret(t, fs, vaultDir)
|
|
|
|
instance, cmd := newTestInstance(fs)
|
|
|
|
found, err := instance.findUnlockerToRemove(listTestUnlockerDirOne)
|
|
require.NoError(t, err)
|
|
assert.False(t, found.last)
|
|
assert.Contains(t, found.question, "not the vault's last unlocker")
|
|
|
|
instance.terminal = strings.NewReader("y\n")
|
|
require.NoError(t, instance.UnlockersRemove(listTestUnlockerDirOne, false, cmd))
|
|
assertDirEntries(t, fs, unlockersDir, listTestUnlockerDirTwo)
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
}
|