package cli_test import ( "bufio" "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/awnumar/memguard" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/secret/internal/cli" "sneak.berlin/go/secret/internal/secret" ) const ( // envRunEntry, when set, makes this test binary run cli.Entry with its // own arguments and exit, instead of running the tests. envRunEntry = "SECRET_TEST_RUN_ENTRY" // bufferWiped is what the test binary prints, run with envRunEntry set, // when cli.Entry wiped the buffer made before it ran. bufferWiped = "Entry wiped the buffer made before it ran" ) // With envRunEntry set, the test binary makes a buffer, runs cli.Entry, // says whether the buffer is wiped, and exits with Entry's exit code. This // is done in init, not in a test, so that TestMain does not run in that // process: it would build the secret binary again, and remove it at the end, // while the other tests run it. // //nolint:gochecknoinits // must run before TestMain, as above func init() { if os.Getenv(envRunEntry) == "" { return } buf := memguard.NewBufferFromBytes([]byte("key material")) code := cli.Entry() if !buf.IsAlive() { fmt.Fprintln(os.Stderr, bufferWiped) } os.Exit(code) } // Entry must return its exit code rather than exit, so that its deferred // memguard purge runs on the success and the error path alike. Entry wipes // every memguard buffer in the process, including those other tests are // using, so it runs in a process of its own: this test binary, run again // with envRunEntry set. func TestEntryWipesBuffersAndReturnsExitCode(t *testing.T) { t.Parallel() tests := []struct { args []string exitCode int }{ {args: []string{"--help"}, exitCode: 0}, {args: []string{"no-such-command"}, exitCode: 1}, } for _, tt := range tests { //nolint:gosec // G204: test runs its own test binary cmd := exec.CommandContext(t.Context(), os.Args[0], tt.args...) cmd.Env = append(os.Environ(), envRunEntry+"=1") output, err := cmd.CombinedOutput() require.NotNil(t, cmd.ProcessState, "%v did not run: %v", tt.args, err) assert.Equal(t, tt.exitCode, cmd.ProcessState.ExitCode(), "exit code for %v: %s", tt.args, output) assert.Contains(t, string(output), bufferWiped, "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(), commandWait) 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) }