Ask before removing a secret, version, vault or unlocker (closes #39)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
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
This commit is contained in:
@@ -16,7 +16,10 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/cli"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/internal/vault"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/awnumar/memguard"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -2542,3 +2545,53 @@ func copyFile(src, dst string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestRemoveWithoutTerminalFailsAtOnce runs `secret rm` without --force,
|
||||
// with a stdin that is not a terminal and never delivers anything, as in a
|
||||
// script or a CI job. It must fail at once, telling the user to pass
|
||||
// --force, instead of waiting for an answer, and remove nothing.
|
||||
func TestRemoveWithoutTerminalFailsAtOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stateDir := t.TempDir()
|
||||
|
||||
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
||||
defer mnemonic.Destroy()
|
||||
|
||||
vlt, err := vault.CreateVault(afero.NewOsFs(), stateDir, "default", mnemonic)
|
||||
require.NoError(t, err)
|
||||
|
||||
value := memguard.NewBufferFromBytes([]byte("value"))
|
||||
defer value.Destroy()
|
||||
|
||||
require.NoError(t, vlt.AddSecret("x", value, false))
|
||||
|
||||
// Nobody writes to or closes the pipe, so reading it would block for good.
|
||||
stdin, stdinWriter, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
_ = stdinWriter.Close()
|
||||
_ = stdin.Close()
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||
cmd := exec.CommandContext(ctx, secretBinaryPath(t), "rm", "x")
|
||||
cmd.Env = []string{
|
||||
secret.EnvStateDir + "=" + stateDir,
|
||||
"PATH=" + os.Getenv("PATH"),
|
||||
"HOME=" + os.Getenv("HOME"),
|
||||
}
|
||||
cmd.Stdin = stdin
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
require.NoError(t, ctx.Err(), "secret rm waited for an answer")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, string(output), "pass --force")
|
||||
assert.DirExists(t,
|
||||
filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "x"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user