check / check (push) Waiting to run
Removing the vault's current unlocker left current-unlocker naming it, so every command that opened the vault failed although another unlocker remained, and the error pointed at the mnemonic. RemoveUnlocker now first makes current the first other unlocker that `secret unlocker select` accepts, in the order `secret unlocker list` shows, and returns its ID, which `secret unlocker remove` prints; with none, current-unlocker is removed too. When current-unlocker names an unlocker that does not exist and the vault has another that select accepts, the error wraps ErrUnlockerNotFound and says to run `secret unlocker select`, without the mnemonic advice. Unverified: no test covers that advice for a vault other than the current one, which gets `secret vault select` first. Model: opus-5-5
95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
//nolint:testpackage // white-box test of unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/secret/internal/secret"
|
|
"sneak.berlin/go/secret/internal/vault"
|
|
)
|
|
|
|
// TestUnlockerRemoveCurrent is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/137. A vault has a passphrase
|
|
// unlocker and a PGP unlocker, the current one. Removing the PGP unlocker
|
|
// makes the passphrase unlocker current and says so, and the vault then
|
|
// opens through it. Removing the passphrase unlocker too removes
|
|
// current-unlocker.
|
|
//
|
|
// The second case also gives the vault the unlocker directory "odd", whose
|
|
// metadata names a type that `secret unlocker select` refuses, and which
|
|
// sorts before the passphrase unlocker: neither removal makes it current.
|
|
//
|
|
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
|
func TestUnlockerRemoveCurrent(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
withOdd bool
|
|
}{
|
|
{name: "two unlockers", withOdd: false},
|
|
{name: "and the directory odd", withOdd: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
newTestGPGKey(t)
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
t.Cleanup(passphrase.Destroy)
|
|
|
|
fs := afero.NewMemMapFs()
|
|
vlt, err := vault.CreateVault(fs, listTestStateDir,
|
|
listTestVaultName, testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphrase)
|
|
require.NoError(t, err)
|
|
|
|
if tt.withOdd {
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(testVaultDir(listTestVaultName), "unlockers.d",
|
|
"odd", "unlocker-metadata.json"),
|
|
[]byte(`{"type": "odd"}`), secret.FilePerms))
|
|
}
|
|
|
|
instance, cmd := newTestInstance(fs)
|
|
instance.UnlockPassphrase = passphrase
|
|
|
|
cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
|
require.NoError(t, instance.UnlockersAdd(unlockerTypePGP, cmd))
|
|
|
|
pgpUnlocker, err := vlt.GetCurrentUnlocker()
|
|
require.NoError(t, err)
|
|
require.Equal(t, unlockerTypePGP, pgpUnlocker.GetType())
|
|
|
|
var output bytes.Buffer
|
|
|
|
cmd.SetOut(&output)
|
|
require.NoError(t,
|
|
instance.UnlockersRemove(pgpUnlocker.GetID(), true, cmd))
|
|
assert.Contains(t, output.String(),
|
|
"Switched current unlocker to '"+passphraseUnlocker.GetID()+"'")
|
|
|
|
reopened := vault.NewVault(fs, listTestStateDir, listTestVaultName)
|
|
reopened.UnlockPassphrase = passphrase
|
|
|
|
_, err = reopened.GetOrDeriveLongTermKey()
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t,
|
|
instance.UnlockersRemove(passphraseUnlocker.GetID(), true, cmd))
|
|
|
|
exists, err := afero.Exists(fs, filepath.Join(
|
|
testVaultDir(listTestVaultName), "current-unlocker"))
|
|
require.NoError(t, err)
|
|
assert.False(t, exists,
|
|
"current-unlocker is left after the passphrase unlocker")
|
|
})
|
|
}
|
|
}
|