From fe317abac7a029441792c3c2b74e08a95b363ec4 Mon Sep 17 00:00:00 2001 From: Josh Kunz Date: Sat, 29 Apr 2023 22:55:40 -0700 Subject: [PATCH] Add test for reconnect with manually entered password. --- src/getpass.cc | 5 + t/integration/integration/integration_test.go | 105 +++++++++++++++++- t/integration/testashuffle/testashuffle.go | 15 ++- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/src/getpass.cc b/src/getpass.cc index 183ed213..b958b042 100644 --- a/src/getpass.cc +++ b/src/getpass.cc @@ -20,6 +20,11 @@ void SetEcho(FILE *stream, bool echo_state, bool echo_nl_state) { struct termios flags; int res = tcgetattr(fileno(stream), &flags); if (res != 0) { + if (errno == ENOTTY) { + // If the output device is not a tty, then we don't need to + // worry about the echo. + return; + } perror("SetEcho (tcgetattr)"); std::exit(1); } diff --git a/t/integration/integration/integration_test.go b/t/integration/integration/integration_test.go index a3964a87..56713f47 100644 --- a/t/integration/integration/integration_test.go +++ b/t/integration/integration/integration_test.go @@ -1,6 +1,8 @@ package integration_test import ( + "bufio" + "bytes" "context" "io" "io/ioutil" @@ -963,6 +965,7 @@ func TestExclude(t *testing.T) { } func TestReconnect(t *testing.T) { + t.Parallel() ctx := context.Background() root, err := testmpd.NewRoot(&testmpd.Options{LibraryRoot: "/music"}) @@ -1030,9 +1033,10 @@ func TestReconnect(t *testing.T) { if err := restartMPD.Shutdown(); err != nil { t.Errorf("restart mpd did not shutdown cleanly: %v", err) } - } + func TestReconnect_Timeout(t *testing.T) { + t.Parallel() ctx := context.Background() as, mpd := run(ctx, t, runOptions{ @@ -1077,3 +1081,102 @@ func TestReconnect_Timeout(t *testing.T) { t.Errorf("ashuffle stderr does not include %q", wantMsg) } } + +func TestReconnect_PasswordPrompt(t *testing.T) { + t.Parallel() + ctx := context.Background() + + mpd, err := testmpd.New(ctx, &testmpd.Options{ + LibraryRoot: "/music", + DefaultPermissions: []string{"read"}, + Passwords: []testmpd.Password{ + { + Password: "super_secret_mpd_password", + Permissions: []string{"read", "add", "control", "admin"}, + }, + }, + }) + if err != nil { + t.Fatalf("failed to create new MPD instance: %v", err) + } + + stdinr, stdinw := io.Pipe() + stdoutr, stdoutw := io.Pipe() + defer stdoutr.Close() + defer stdoutw.Close() + + as, err := testashuffle.New(ctx, ashuffleBin, &testashuffle.Options{ + MPDAddress: mpd, + Args: []string{"--tweak", "reconnect-timeout=10s"}, + Stdin: stdinr, + Stdout: stdoutw, + ShutdownTimeout: 30 * time.Second, + }) + if err != nil { + t.Fatalf("failed to start ashuffle: %v", err) + } + + scanStdout := bufio.NewScanner(stdoutr) + scanStdout.Split(func(data []byte, _ bool) (int, []byte, error) { + idx := bytes.IndexRune(data, ':') + if idx < 0 { + return 0, nil, nil + } + return idx, data[:idx+1], nil + }) + for scanStdout.Scan() { + if strings.Contains(scanStdout.Text(), "mpd password:") { + break + } + } + if err := scanStdout.Err(); err != nil { + t.Fatalf("scanner threw error: %v", err) + } + + // Make sure that stdout Pipe doesn't block future writes. + go io.Copy(io.Discard, stdoutr) + + // Enter the password. + if _, err := io.WriteString(stdinw, "super_secret_mpd_password\n"); err != nil { + t.Fatalf("failed to write password to ashuffle stdin: %v", err) + } + + // Close out stdin now that we've entered our password. + stdinr.Close() + stdinw.Close() + + // Wait for ashuffle to startup, and start playing a song. + tryWaitFor(func() bool { return mpd.PlayState() == testmpd.StatePlay }) + + if state := mpd.PlayState(); state != testmpd.StatePlay { + t.Errorf("[before shutdown] mpd.PlayState() = %v, want play", state) + } + + // Shutdown MPD, ashuffle should remain running. But we won't actually + // know till later. Because we entered the password manually this should + // immediately exit ashuffle. + if err := mpd.Shutdown(); err != nil { + t.Fatalf("Failed to shutdown MPD: %v", err) + } + if !mpd.IsOk() { + t.Errorf("MPD had errors: %+v", mpd.Errors) + } + + err = as.Shutdown(testashuffle.ShutdownSoft) + if err == nil { + t.Logf("stdout:\n%s", as.Stdout) + t.Logf("stderr:\n%s", as.Stderr) + t.Fatalf("ashuffle should have exited with an error, but shutdown cleanly instead") + } + + exitErr, ok := err.(*exec.ExitError) + if !ok { + t.Logf("stdout:\n%s", as.Stdout) + t.Logf("stderr:\n%s", as.Stderr) + t.Fatalf("ashuffle shutdown did not produce exit error, produced %#v", err) + } + + if got := exitErr.ExitCode(); got != 1 { + t.Errorf("ashuffle exited with code %d, want 1 (full err: %v)", got, err) + } +} diff --git a/t/integration/testashuffle/testashuffle.go b/t/integration/testashuffle/testashuffle.go index 8d5ea4c9..7d02d165 100644 --- a/t/integration/testashuffle/testashuffle.go +++ b/t/integration/testashuffle/testashuffle.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -143,6 +144,8 @@ type Options struct { Args []string EnableHeapProfile bool ShutdownTimeout time.Duration + Stdin io.Reader + Stdout io.Writer } func New(ctx context.Context, path string, opts *Options) (*Ashuffle, error) { @@ -169,7 +172,17 @@ func New(ctx context.Context, path string, opts *Options) (*Ashuffle, error) { var stdout, stderr bytes.Buffer cmd.Stderr = &stderr - cmd.Stdout = &stdout + if opts != nil && opts.Stdout != nil { + // If the user provides their own stdout channel, then tee between + // both the buffer, and their channel. + cmd.Stdout = io.MultiWriter(opts.Stdout, &stdout) + } else { + cmd.Stdout = &stdout + } + + if opts != nil && opts.Stdin != nil { + cmd.Stdin = opts.Stdin + } shutdownTimeout := maxShutdownWait if opts != nil && opts.ShutdownTimeout != 0 {