check / check (push) Successful in 1m1s
On a case-insensitive filesystem (the macOS default) "Foo" and "foo" name one secret, so `secret mv --force Foo foo` removed the destination, which was the source, and lost the secret with every version. Between vaults the copy replaced the source, and removing the source then removed the copy. Both kinds of move now compare the two secret directories with os.SameFile before changing anything and reject the move if they are one, with or without --force. The tests give one secret two names with symbolic links on the real filesystem. Model: opus-5-5
230 lines
7.2 KiB
Go
230 lines
7.2 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"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"
|
|
)
|
|
|
|
// 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.
|
|
//
|
|
//nolint:paralleltest // newTwoVaultFs uses t.Setenv
|
|
func TestRejectedMoveWithinVaultLeavesStateUnchanged(t *testing.T) {
|
|
before := snapshotStateDir(t, newTwoVaultFs(t))
|
|
require.Equal(t, "default", before[testStateDir+"/currentvault"])
|
|
|
|
const (
|
|
ontoItself = "secret 'x' cannot be moved onto itself"
|
|
workX = "work:x"
|
|
)
|
|
|
|
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, so ".." cannot reach the state
|
|
// directory itself.
|
|
{
|
|
"mv --force ..:x ..:y", "..:x", "..:y", true,
|
|
"vault '..' does not exist",
|
|
},
|
|
// Each of these spells "work" a second way. The spelling is not an
|
|
// existing vault name, so the move is not taken for a move between
|
|
// two vaults, which would delete the destination, here the source.
|
|
{
|
|
"mv --force work:x work/:x", workX, "work/:x", true,
|
|
"vault 'work/' does not exist",
|
|
},
|
|
{
|
|
"mv --force work/:x work:", "work/:x", "work:", true,
|
|
"vault 'work/' does not exist",
|
|
},
|
|
{
|
|
"mv --force work:x ./work:x", workX, "./work:x", true,
|
|
"vault './work' does not exist",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.command, func(t *testing.T) {
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
//nolint:paralleltest // newTwoVaultFs uses t.Setenv
|
|
func TestMoveWithinOtherVaultKeepsCurrentVault(t *testing.T) {
|
|
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.
|
|
//
|
|
//nolint:paralleltest // t.Setenv
|
|
func TestMoveOntoSameSecretUnderAnotherNameIsRejected(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
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) {
|
|
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")
|
|
require.NoError(t, err)
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default")
|
|
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)
|
|
require.Equal(t, "value", string(value))
|
|
|
|
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.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
fs := afero.NewOsFs()
|
|
stateDir := t.TempDir()
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, "default")
|
|
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)
|
|
require.Equal(t, "upper", string(value))
|
|
|
|
_, err = vlt.GetSecret("Foo")
|
|
require.ErrorIs(t, err, vault.ErrSecretNotFound)
|
|
}
|