check / check (push) Failing after 19s
Vault.GetSecret and Vault.GetSecretVersion return the decrypted value as a *memguard.LockedBuffer instead of copying it into an ordinary []byte that nothing wiped. Every caller destroys the buffer, and `secret get` writes its bytes straight to stdout, still with no trailing newline. Instance.Print, which formatted through fmt and had no other callers, is removed, and so is a debug log line in `get --version` that held the plaintext value. Model: opus-5-5
154 lines
4.7 KiB
Go
154 lines
4.7 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/cli"
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestCreateExistingVaultChangesNothing is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/74, where running `secret init`
|
|
// a second time, or `secret vault create` with the name of an existing
|
|
// vault, replaced that vault's keys, so that none of its secrets could be
|
|
// decrypted any more. Each must refuse, change nothing, and leave every
|
|
// vault's secret readable through its passphrase unlocker.
|
|
//
|
|
//nolint:paralleltest // t.Setenv forbids parallel subtests
|
|
func TestCreateExistingVaultChangesNothing(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
|
|
|
|
// `secret init`, `secret vault create work`, `secret vault select
|
|
// default`, and the secret "x" in each vault. "work" is then not the
|
|
// current vault, which creating it again must not change.
|
|
fs := afero.NewMemMapFs()
|
|
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
cmd := &cobra.Command{}
|
|
|
|
require.NoError(t, c.Init(cmd))
|
|
require.NoError(t, c.CreateVault(cmd, "work"))
|
|
require.NoError(t, c.SelectVault(cmd, "default"))
|
|
|
|
vaults, err := vault.ListVaults(fs, testStateDir)
|
|
require.NoError(t, err)
|
|
require.Len(t, vaults, 2)
|
|
|
|
for _, name := range vaults {
|
|
value := memguard.NewBufferFromBytes([]byte("value"))
|
|
err := vault.NewVault(fs, testStateDir, name).AddSecret("x", value, false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
before := snapshotStateDir(t, fs)
|
|
|
|
tests := []struct {
|
|
command string
|
|
want string
|
|
run func(c *cli.Instance) error
|
|
}{
|
|
{
|
|
"init",
|
|
"failed to create default vault: vault default already exists",
|
|
func(c *cli.Instance) error { return c.Init(cmd) },
|
|
},
|
|
{
|
|
"vault create default",
|
|
"vault default already exists",
|
|
func(c *cli.Instance) error { return c.CreateVault(cmd, "default") },
|
|
},
|
|
{
|
|
"vault create work",
|
|
"vault work already exists",
|
|
func(c *cli.Instance) error { return c.CreateVault(cmd, "work") },
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
fs := newFsFromSnapshot(t, before)
|
|
|
|
err := tt.run(cli.NewCLIInstanceWithStateDir(fs, testStateDir))
|
|
|
|
require.EqualError(t, err, tt.want)
|
|
require.Equal(t, before, snapshotStateDir(t, fs))
|
|
})
|
|
}
|
|
|
|
// Every case left the state directory exactly as recorded in before, so
|
|
// reading each vault's secret once from it shows that it still decrypts
|
|
// after each case. Without the mnemonic, reading a secret goes through
|
|
// the vault's passphrase unlocker, which is slow.
|
|
t.Setenv(secret.EnvMnemonic, "")
|
|
|
|
for _, name := range vaults {
|
|
value, err := vault.NewVault(fs, testStateDir, name).GetSecret("x")
|
|
require.NoError(t, err)
|
|
|
|
unchanged := bytes.Equal([]byte("value"), value.Bytes())
|
|
value.Destroy()
|
|
|
|
require.True(t, unchanged, "vault %q kept its secret", name)
|
|
}
|
|
}
|
|
|
|
// TestStopAtPassphrasePromptLeavesNothing is a regression test for the
|
|
// review of https://git.eeqj.de/sneak/secret/pulls/82: `secret init` or
|
|
// `secret vault create` stopped at the passphrase prompt left a vault with
|
|
// no unlocker, which neither command would then create again. Each must ask
|
|
// for the passphrase before writing anything.
|
|
//
|
|
//nolint:paralleltest // t.Setenv forbids parallel subtests
|
|
func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
// Without the passphrase in the environment, both commands prompt for
|
|
// it, which fails because the tests do not run in a terminal.
|
|
t.Setenv(secret.EnvUnlockPassphrase, "")
|
|
|
|
// An empty state directory for `secret init`, and one holding the vault
|
|
// "default" for `secret vault create work`.
|
|
empty := afero.NewMemMapFs()
|
|
require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms))
|
|
|
|
withDefault := afero.NewMemMapFs()
|
|
_, err := vault.CreateVault(withDefault, testStateDir, "default")
|
|
require.NoError(t, err)
|
|
|
|
cmd := &cobra.Command{}
|
|
|
|
tests := []struct {
|
|
command string
|
|
fs afero.Fs
|
|
run func(c *cli.Instance) error
|
|
}{
|
|
{
|
|
"init",
|
|
empty,
|
|
func(c *cli.Instance) error { return c.Init(cmd) },
|
|
},
|
|
{
|
|
"vault create work",
|
|
withDefault,
|
|
func(c *cli.Instance) error { return c.CreateVault(cmd, "work") },
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
before := snapshotStateDir(t, tt.fs)
|
|
|
|
err := tt.run(cli.NewCLIInstanceWithStateDir(tt.fs, testStateDir))
|
|
|
|
require.ErrorContains(t, err, "failed to read passphrase")
|
|
require.Equal(t, before, snapshotStateDir(t, tt.fs))
|
|
})
|
|
}
|
|
}
|