package cli_test import ( "bufio" "io" "os" "os/exec" "strings" "testing" "time" "github.com/creack/pty" "github.com/stretchr/testify/require" "golang.org/x/term" "sneak.berlin/go/keyfunc/internal/mnemonic" ) // TestEndOfInputAtThePromptEndsTheTool runs "age pub" with no mnemonic // given and a terminal on all three streams, waits for the prompt, and // types Ctrl-D. The tool has to end with status 1, say that the // mnemonic is empty, and leave the terminal set as it found it, so with // echo on again. // // pty.Open returns the two ends of a new terminal: tty is the end the // tool uses as its terminal, and ptmx the end the test reads what the // terminal shows from and types into. Reading ptmx stops at the // deadline, so a prompt that never comes fails the test instead of // hanging it. func TestEndOfInputAtThePromptEndsTheTool(t *testing.T) { t.Setenv(mnemonic.Variable, "") t.Setenv(mnemonic.CommandVariable, "") ptmx, tty, err := pty.Open() require.NoError(t, err) defer func() { _ = ptmx.Close() }() defer func() { _ = tty.Close() }() require.NoError(t, ptmx.SetReadDeadline(time.Now().Add(10*time.Second))) before, err := term.GetState(int(tty.Fd())) require.NoError(t, err) //nolint:gosec // this test's own binary as the tool command := exec.CommandContext(t.Context(), os.Args[0], "age", "pub") command.Env = append(os.Environ(), runAsTool+"=1") command.Stdin = tty command.Stdout = tty command.Stderr = tty require.NoError(t, command.Start()) var ( shown string char byte ) terminal := bufio.NewReader(ptmx) for !strings.HasSuffix(shown, "mnemonic: ") { char, err = terminal.ReadByte() require.NoError(t, err, "the tool did not prompt: %q", shown) shown += string(char) } _, err = ptmx.WriteString("\x04") // Ctrl-D require.NoError(t, err) waitForTool(t, "Ctrl-D at the prompt", command) require.Equal(t, failedStatus, command.ProcessState.ExitCode()) after, err := term.GetState(int(tty.Fd())) require.NoError(t, err) require.Equal(t, before, after, "the terminal settings were not restored") // With the tool ended and this end closed too, ptmx gives what is // left on the terminal and then an error. require.NoError(t, tty.Close()) said, _ := io.ReadAll(terminal) require.Contains(t, string(said), mnemonic.ErrEmpty.Error()) }