package cli_test import ( "bufio" "context" "os" "os/exec" "path/filepath" "strings" "testing" "time" "git.eeqj.de/sneak/secret/internal/cli" "git.eeqj.de/sneak/secret/internal/secret" "github.com/awnumar/memguard" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Entry must return its exit code rather than exit, so that its deferred // memguard purge runs on the success and the error path alike. // //nolint:paralleltest // sets os.Args, and Entry wipes every buffer in the process func TestEntryWipesBuffersAndReturnsExitCode(t *testing.T) { savedArgs := os.Args t.Cleanup(func() { os.Args = savedArgs }) tests := []struct { args []string exitCode int }{ {args: []string{"secret", "--help"}, exitCode: 0}, {args: []string{"secret", "no-such-command"}, exitCode: 1}, } for _, tt := range tests { buf := memguard.NewBufferFromBytes([]byte("key material")) os.Args = tt.args assert.Equal(t, tt.exitCode, cli.Entry(), "exit code for %v", tt.args) assert.False(t, buf.IsAlive(), "Entry left a buffer unwiped for %v", tt.args) } } // Ctrl-C while `secret add` waits for the value on stdin must end the // process through memguard's signal handler, which wipes every buffer and // exits with status 1, not through Go's default handling, which kills the // process with the buffers intact. func TestInterruptExitsThroughMemguard(t *testing.T) { t.Parallel() const waitingForValue = "Reading secret value from stdin" ctx, cancel := context.WithTimeout(t.Context(), time.Minute) defer cancel() wd, err := filepath.Abs("../..") require.NoError(t, err) secretPath := filepath.Join(wd, "secret") env := []string{ secret.EnvStateDir + "=" + t.TempDir(), secret.EnvMnemonic + "=" + testMnemonic, secret.EnvUnlockPassphrase + "=test-passphrase", "PATH=/usr/bin:/bin", // The debug log on stderr shows when add starts waiting for the value. "GODEBUG=berlin.sneak.pkg.secret", } //nolint:gosec // G204: test executes the freshly built secret binary initCmd := exec.CommandContext(ctx, secretPath, "init") initCmd.Env = env output, err := initCmd.CombinedOutput() require.NoError(t, err, "init should succeed: %s", output) //nolint:gosec // G204: test executes the freshly built secret binary addCmd := exec.CommandContext(ctx, secretPath, "add", "test/secret") addCmd.Env = env // Held open and never written, so add keeps waiting for the value. stdin, err := addCmd.StdinPipe() require.NoError(t, err) defer func() { _ = stdin.Close() }() stderr, err := addCmd.StderrPipe() require.NoError(t, err) require.NoError(t, addCmd.Start()) waiting := false scanner := bufio.NewScanner(stderr) for !waiting && scanner.Scan() { waiting = strings.Contains(scanner.Text(), waitingForValue) } require.True(t, waiting, "add never logged %q", waitingForValue) require.NoError(t, addCmd.Process.Signal(os.Interrupt)) err = addCmd.Wait() var exitErr *exec.ExitError require.ErrorAs(t, err, &exitErr) assert.Equal(t, 1, exitErr.ExitCode(), "add ended with %v", err) }