check / check (push) Failing after 4s
Tests that asserted a failure by a fragment of its message now use errors.Is: a refactor returning the wrong error, or wrapping with %v instead of %w, now fails them. New tests return each exported error of internal/vault and pkg/bip85 that no test returned, and check wrapped causes (os.ErrNotExist, ErrMnemonicMismatch through GetSecret, ErrInvalidPathComponent through DeriveBIP85Entropy). The 999-versions test moves into package secret to name its unexported error. Checks of errors no test can name keep their text; they are listed on the issue. Model: opus-5-5
243 lines
7.4 KiB
Go
243 lines
7.4 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/cli"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestRejectedMoveWithinVaultLeavesStateUnchanged is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/73, where a forced move of a secret
|
|
// onto itself deleted it, also when "work" was spelled two ways, and a failed
|
|
// move within "work" left "work" the current vault. "default" is the current
|
|
// vault in every case, and each case runs on its own copy of the state
|
|
// directory.
|
|
func TestRejectedMoveWithinVaultLeavesStateUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
before := snapshotStateDir(t, newTwoVaultFs(t))
|
|
require.Equal(t, "default", before[testStateDir+"/currentvault"])
|
|
|
|
const (
|
|
ontoItself = "secret 'x' cannot be moved onto itself"
|
|
workX = "work:x"
|
|
)
|
|
|
|
// internal/cli declares these errors itself and does not export them, so
|
|
// only their text can be compared.
|
|
tests := []struct {
|
|
command string
|
|
source, dest string
|
|
force bool
|
|
wantErr string
|
|
}{
|
|
{"mv x x", "x", "x", false, ontoItself},
|
|
{"mv --force x x", "x", "x", true, ontoItself},
|
|
{"mv --force work:x work:", workX, "work:", true, ontoItself},
|
|
// An empty destination name defaults to the source name.
|
|
{`mv --force work:x ""`, workX, "", true, ontoItself},
|
|
// "work" is a vault name, so the destination is work:x.
|
|
{"mv --force work:x work", workX, "work", true, ontoItself},
|
|
{
|
|
"mv work:nosuch work:y", "work:nosuch", "work:y", false,
|
|
"secret 'nosuch' not found",
|
|
},
|
|
// Only an existing vault is used.
|
|
{
|
|
"mv --force nosuch:x nosuch:y", "nosuch:x", "nosuch:y", true,
|
|
"vault 'nosuch' does not exist",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := newFsFromSnapshot(t, before)
|
|
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
|
|
err := c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, tt.force)
|
|
|
|
require.Equal(t, before, snapshotStateDir(t, fs))
|
|
require.EqualError(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
|
|
// Each of these spells "work" a second way. The spelling is not a valid
|
|
// vault name, so the move is not taken for a move between two vaults,
|
|
// which would delete the destination, here the source.
|
|
invalidNames := []struct{ source, dest string }{
|
|
{workX, "work/:x"},
|
|
{"work/:x", "work:"},
|
|
{workX, "./work:x"},
|
|
}
|
|
|
|
for _, tt := range invalidNames {
|
|
t.Run("mv --force "+tt.source+" "+tt.dest, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
requireRejectedAndUnchanged(t, before, vault.ErrInvalidVaultName,
|
|
func(c *cli.Instance) error {
|
|
return c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, true)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMoveWithinOtherVaultKeepsCurrentVault checks that `secret mv work:x
|
|
// work:y`, with "default" the current vault, renames "x" to "y" in "work" and
|
|
// leaves "default" the current vault.
|
|
func TestMoveWithinOtherVaultKeepsCurrentVault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := newTwoVaultFs(t)
|
|
c := cli.NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
|
|
err := c.MoveSecret(&cobra.Command{}, "work:x", "work:y", false)
|
|
require.NoError(t, err)
|
|
|
|
after := snapshotStateDir(t, fs)
|
|
workSecrets := testStateDir + "/vaults.d/work/secrets.d/"
|
|
|
|
require.Equal(t, "default", after[testStateDir+"/currentvault"])
|
|
require.Contains(t, after, workSecrets+"y/")
|
|
require.NotContains(t, after, workSecrets+"x/")
|
|
}
|
|
|
|
// TestMoveOntoSameSecretUnderAnotherNameIsRejected is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/78: on a case-insensitive
|
|
// filesystem "Foo" and "foo" are one secret, and `secret mv --force Foo foo`
|
|
// removed the destination, which was the source. Symbolic links on the real
|
|
// filesystem give one secret two names here: in "default", "y" is a link to
|
|
// the secret "x", and the secrets.d of "other" is a link to that of
|
|
// "default", so other:x is default:x. Each move must be rejected and leave
|
|
// the secret and the links as they were.
|
|
func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const isSame = "is the same secret on this filesystem"
|
|
|
|
tests := []struct {
|
|
command string
|
|
source, dest string
|
|
force bool
|
|
wantErr string
|
|
}{
|
|
{
|
|
"mv --force y x", "y", "x", true,
|
|
"secret 'y' cannot be moved onto itself: 'x' " + isSame,
|
|
},
|
|
{
|
|
"mv --force x y", "x", "y", true,
|
|
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
|
|
},
|
|
{
|
|
"mv x y", "x", "y", false,
|
|
"secret 'x' cannot be moved onto itself: 'y' " + isSame,
|
|
},
|
|
{
|
|
"mv --force default:x other:x", "default:x", "other:x", true,
|
|
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
|
|
isSame,
|
|
},
|
|
{
|
|
"mv default:x other", "default:x", "other", false,
|
|
"secret 'default:x' cannot be moved onto itself: 'other:x' " +
|
|
isSame,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
vaultsDir := filepath.Join(stateDir, "vaults.d")
|
|
|
|
// "default" is created last, so it is the current vault.
|
|
_, err := vault.CreateVault(fs, stateDir, "other", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("value")), false)
|
|
require.NoError(t, err)
|
|
|
|
defaultSecrets := filepath.Join(vaultsDir, "default", "secrets.d")
|
|
otherSecrets := filepath.Join(vaultsDir, "other", "secrets.d")
|
|
link := filepath.Join(defaultSecrets, "y")
|
|
|
|
require.NoError(t, os.Symlink("x", link))
|
|
require.NoError(t, os.Remove(otherSecrets))
|
|
require.NoError(t, os.Symlink(defaultSecrets, otherSecrets))
|
|
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
moveErr := c.MoveSecret(&cobra.Command{}, tt.source, tt.dest, tt.force)
|
|
|
|
value, err := vlt.GetSecret("x")
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
require.Equal(t, []byte("value"), value.Bytes())
|
|
|
|
target, err := os.Readlink(link)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "x", target)
|
|
|
|
target, err = os.Readlink(otherSecrets)
|
|
require.NoError(t, err)
|
|
require.Equal(t, defaultSecrets, target)
|
|
|
|
require.EqualError(t, moveErr, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem checks that where "Foo"
|
|
// and "foo" are two secrets, `secret mv --force Foo foo` still replaces "foo"
|
|
// with "Foo".
|
|
func TestForcedCaseOnlyMoveOnCaseSensitiveFilesystem(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
err = vlt.AddSecret("Foo", memguard.NewBufferFromBytes([]byte("upper")), false)
|
|
require.NoError(t, err)
|
|
|
|
_, err = os.Stat(filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "foo"))
|
|
if err == nil {
|
|
t.Skip("the temporary directory is on a case-insensitive filesystem")
|
|
}
|
|
|
|
err = vlt.AddSecret("foo", memguard.NewBufferFromBytes([]byte("lower")), false)
|
|
require.NoError(t, err)
|
|
|
|
c := cli.NewCLIInstanceWithStateDir(fs, stateDir)
|
|
err = c.MoveSecret(&cobra.Command{}, "Foo", "foo", true)
|
|
require.NoError(t, err)
|
|
|
|
value, err := vlt.GetSecret("foo")
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
require.Equal(t, []byte("upper"), value.Bytes())
|
|
|
|
_, err = vlt.GetSecret("Foo")
|
|
require.ErrorIs(t, err, vault.ErrSecretNotFound)
|
|
}
|