package cli import ( "bytes" "context" "encoding/json" "errors" "net" "path/filepath" "strings" "testing" ) // A request and an answer survive a round trip, bytes and all. func TestARequestAndAnAnswerSurviveTheirRoundTrip(t *testing.T) { var b bytes.Buffer line := []string{"settings", "set", "zsh", `{"execute":"withhold"}`, "--node", "a"} if err := WriteRequest(&b, Request{Line: line}); err != nil { t.Fatal(err) } got, err := ReadRequest(&b) if err != nil { t.Fatal(err) } if strings.Join(got.Line, "\x00") != strings.Join(line, "\x00") { t.Fatalf("the line came back as %q", got.Line) } b.Reset() sent := Answer{Stdout: []byte("ok\n\xff"), Stderr: []byte("said"), Exit: 3, Terminal: true, Why: "the terminal", Cut: true, Unknown: "no answer; the line may have run — see the call call-1"} if err := WriteAnswer(&b, sent); err != nil { t.Fatal(err) } a, err := ReadAnswer(&b) if err != nil { t.Fatal(err) } if !bytes.Equal(a.Stdout, sent.Stdout) || string(a.Stderr) != "said" || a.Exit != 3 || !a.Terminal || a.Why != sent.Why || !a.Cut || a.Unknown != sent.Unknown { t.Fatalf("the answer came back as %+v", a) } } // A request over the bound is refused, not read whole; one naming no command is refused. func TestARequestOverTheBoundOrNamingNothingIsRefused(t *testing.T) { long := Request{Line: []string{strings.Repeat("x", MaxRequest)}} big := Request{Line: []string{"secret"}, Stdin: bytes.Repeat([]byte("s"), MaxStdin+1)} var bb bytes.Buffer if err := WriteRequest(&bb, big); err != nil { t.Fatal(err) } if _, err := ReadRequest(&bb); !errors.Is(err, ErrTooLong) { t.Fatalf("standard input over its bound was read: %v", err) } if _, err := AskWith(context.Background(), "/nonexistent.sock", big); err == nil || !strings.Contains(err.Error(), "nothing was asked") { t.Fatalf("mesh-cli would ask with standard input over its bound: %v", err) } var b bytes.Buffer if err := WriteRequest(&b, long); err != nil { t.Fatal(err) } if _, err := ReadRequest(&b); !errors.Is(err, ErrTooLong) { t.Fatalf("a request over the bound was read: %v", err) } if _, err := ReadRequest(strings.NewReader(`{"line":[]}` + "\n")); err == nil { t.Fatal("a request naming no command was taken") } if _, err := ReadRequest(strings.NewReader("not json\n")); err == nil { t.Fatal("a request that is not one was taken") } } // Ask speaks the protocol over a socket, as mesh-cli does to the node-engine. func TestAskSpeaksTheProtocolOverASocket(t *testing.T) { socket := filepath.Join(t.TempDir(), "cli.sock") l, err := net.Listen("unix", socket) if err != nil { t.Skipf("no unix socket here: %v", err) } defer l.Close() go func() { conn, err := l.Accept() if err != nil { return } defer conn.Close() req, err := ReadRequest(conn) if err != nil { _ = WriteAnswer(conn, Refusal(err.Error())) return } _ = WriteAnswer(conn, Answer{Stdout: []byte(strings.Join(req.Line, " "))}) }() a, err := Ask(context.Background(), socket, []string{"status"}) if err != nil { t.Fatal(err) } if string(a.Stdout) != "status" || a.Exit != 0 || a.Refused != "" { t.Fatalf("asked status and heard %+v", a) } if _, err := Ask(context.Background(), filepath.Join(t.TempDir(), "none.sock"), []string{"status"}); err == nil || !strings.Contains(err.Error(), "does not answer") { t.Fatalf("a missing socket was not said: %v", err) } } // An outcome not known is never a refusal: it says the line may have run, and exits non-zero. func TestAnUnknownOutcomeIsNotARefusal(t *testing.T) { a := NotKnown("call-7", "no answer within 6m0s") if a.Refused != "" || a.Exit == 0 || !strings.Contains(a.Unknown, "may have run") || !strings.Contains(a.Unknown, "call-7") { t.Fatalf("an unknown outcome reads as %+v", a) } if b := NotKnown("", "no answer"); strings.Contains(b.Unknown, "call ") { t.Fatalf("an unknown outcome with no call names one: %+v", b) } } // Standard input up to its bound travels whole, beside the line: a secret given with --from -. func TestStandardInputTravelsWithItsLine(t *testing.T) { given := append(bytes.Repeat([]byte{0, 0xff, '\n', 'x'}, MaxStdin/4-1), "end"...) var b bytes.Buffer if err := WriteRequest(&b, Request{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"}, Stdin: given}); err != nil { t.Fatal(err) } if b.Len() > MaxRequest { t.Fatalf("a request with standard input at its bound is %d bytes, over MaxRequest %d", b.Len(), MaxRequest) } got, err := ReadRequest(&b) if err != nil { t.Fatal(err) } if !bytes.Equal(got.Stdin, given) || got.Line[1] != "accept" { t.Fatalf("standard input did not travel whole: %d bytes, want %d", len(got.Stdin), len(given)) } none, _ := json.Marshal(Request{Line: []string{"status"}}) if strings.Contains(string(none), "stdin") { t.Fatalf("a request without standard input says it has one: %s", none) } }