check / check (push) Failing after 3s
secret rm, secret version rm, secret vault remove and secret unlocker remove ask [y/N] on a terminal, naming what they remove, and go ahead only on y or yes. Without --force, a command whose stdin is not a terminal fails at once. --force, now also on rm and version rm, removes without asking; it replaces the old refusals to remove a vault with secrets or the last unlocker without --force. The checks run and the question is asked before the state directory lock is taken; under the lock the checks run again, and nothing is removed if they would ask a different question. Model: opus-5-5
163 lines
5.1 KiB
Go
163 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
|
|
// 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)
|
|
})
|
|
}
|
|
}
|