package diagnostics import ( "context" "errors" "fmt" "net" "os" "reflect" "strings" "sync" "testing" "time" ) type fakeExecutor struct { path string args []string maxOutput int result Execution err error } type waitingExecutor struct{} type executorCall struct { path string args []string } type sequenceExecutor struct { calls []executorCall results []Execution index int } func (e *sequenceExecutor) Execute(_ context.Context, path string, args []string, _ int) (Execution, error) { e.calls = append(e.calls, executorCall{path: path, args: append([]string(nil), args...)}) if e.index >= len(e.results) { return Execution{}, errors.New("unexpected diagnostic execution") } result := e.results[e.index] e.index++ return result, nil } func (waitingExecutor) Execute(ctx context.Context, _ string, _ []string, _ int) (Execution, error) { <-ctx.Done() return Execution{}, ctx.Err() } func (e *fakeExecutor) Execute(_ context.Context, path string, args []string, maxOutput int) (Execution, error) { e.path = path e.args = append([]string(nil), args...) e.maxOutput = maxOutput return e.result, e.err } func TestProcessRunnerUsesFixedExecutableAndArguments(t *testing.T) { executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}} runner, err := NewProcessRunner(ProcessRunnerOptions{ PingPath: "/opt/tera/bin/ping", TraceroutePath: "/opt/tera/bin/traceroute", Executor: executor, }) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 30, MaxOutputBytes: 4096}) if err != nil { t.Fatalf("Run() error = %v", err) } if executor.path != "/opt/tera/bin/ping" || !reflect.DeepEqual(executor.args, []string{"-c", "4", "-s", "56", "-W", "1", "-w", "5", "192.0.2.1"}) { t.Fatalf("command = %q %#v", executor.path, executor.args) } if executor.maxOutput != 4096 || result.Reachable == nil || !*result.Reachable { t.Fatalf("execution limit/result = %d %+v", executor.maxOutput, result) } } func TestProcessRunnerUsesTTLIncrementingPingTraceroute(t *testing.T) { executor := &sequenceExecutor{results: []Execution{ {Output: "From 192.0.2.1 icmp_seq=1 Time to live exceeded", ExitCode: 1}, {Output: "64 bytes from 192.0.2.10: icmp_seq=1 ttl=63 time=1.25 ms", ExitCode: 0}, }} runner, err := NewProcessRunner(ProcessRunnerOptions{ PingPath: "/opt/tera/bin/ping", TraceroutePath: "/opt/tera/bin/traceroute", Executor: executor, }) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 24, TimeoutSeconds: 77, MaxOutputBytes: 4096}) if err != nil { t.Fatalf("Run() error = %v", err) } wantCalls := []executorCall{ {path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "1", "192.0.2.10"}}, {path: "/opt/tera/bin/ping", args: []string{"-c", "1", "-s", "56", "-W", "1", "-t", "2", "192.0.2.10"}}, } if !reflect.DeepEqual(executor.calls, wantCalls) { t.Fatalf("commands = %#v", executor.calls) } if result.Reachable == nil || !*result.Reachable || result.MaxHops != 24 || result.ExitCode != 0 { t.Fatalf("result = %+v, want reachable traceroute", result) } if !strings.Contains(result.Output, " 1 192.0.2.1") || !strings.Contains(result.Output, " 2 192.0.2.10 1.25 ms") { t.Fatalf("output = %q", result.Output) } if operationResult := result.OperationResult(77, 4096); operationResult["max_hops"] != 24 { t.Fatalf("operation result = %#v, want max_hops 24", operationResult) } } func TestProcessRunnerPingTracerouteStopsOnHostUnreachable(t *testing.T) { executor := &sequenceExecutor{results: []Execution{ {Output: "From 192.0.2.1 icmp_seq=1 Destination Host Unreachable", ExitCode: 1}, }} runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor}) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{Kind: KindTraceroute, Target: "192.0.2.10", MaxHops: 10, TimeoutSeconds: 35, MaxOutputBytes: 1024}) if err != nil { t.Fatal(err) } if len(executor.calls) != 1 || result.Reachable == nil || *result.Reachable || result.ExitCode != 1 || !strings.Contains(result.Output, "!H") { t.Fatalf("calls/result = %#v %+v", executor.calls, result) } } func TestProcessRunnerTreatsDiagnosticExitAsResult(t *testing.T) { executor := &fakeExecutor{result: Execution{Output: "unreachable", Bytes: 11, ExitCode: 1}} runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor}) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "example.test", TimeoutSeconds: 5, MaxOutputBytes: 1024}) if err != nil { t.Fatalf("Run() error = %v", err) } if result.ExitCode != 1 || result.Reachable == nil || *result.Reachable { t.Fatalf("result = %+v, want unreachable exit 1", result) } } func TestProcessRunnerAppliesRequestedPingCount(t *testing.T) { executor := &fakeExecutor{result: Execution{Output: "ok", Bytes: 2, ExitCode: 0}} runner, err := NewProcessRunner(ProcessRunnerOptions{ PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: executor, }) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{ Kind: KindNetworkTest, Target: "192.0.2.1", Count: 5, TimeoutSeconds: 7, MaxOutputBytes: 1024, }) if err != nil { t.Fatal(err) } wantArgs := []string{"-c", "5", "-s", "56", "-W", "1", "-w", "6", "192.0.2.1"} if !reflect.DeepEqual(executor.args, wantArgs) || result.Count != 5 { t.Fatalf("args/result = %#v %+v, want count 5", executor.args, result) } if operationResult := result.OperationResult(7, 1024); operationResult["count"] != 5 { t.Fatalf("operation result = %#v, want count 5", operationResult) } } func TestValidateRequestRejectsInvalidPingCount(t *testing.T) { for _, count := range []int{-1, 21} { request := Request{Kind: KindNetworkTest, Target: "192.0.2.1", Count: count, TimeoutSeconds: 5, MaxOutputBytes: 1024} if err := ValidateRequest(request); err == nil { t.Fatalf("ValidateRequest() accepted ping count %d", count) } } } func TestValidateRequestRejectsInvalidTracerouteMaxHops(t *testing.T) { for _, maxHops := range []int{-1, MaximumTracerouteMaxHops + 1} { request := Request{Kind: KindTraceroute, Target: "192.0.2.1", MaxHops: maxHops, TimeoutSeconds: 30, MaxOutputBytes: 1024} if err := ValidateRequest(request); err == nil { t.Fatalf("ValidateRequest() accepted traceroute max hops %d", maxHops) } } } func TestProcessRunnerEnforcesRequestTimeout(t *testing.T) { runner, err := NewProcessRunner(ProcessRunnerOptions{ PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute", Executor: waitingExecutor{}, }) if err != nil { t.Fatal(err) } started := time.Now() _, err = runner.Run(context.Background(), Request{Kind: KindNetworkTest, Target: "192.0.2.1", TimeoutSeconds: 1, MaxOutputBytes: 1024}) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Run() error = %v, want deadline exceeded", err) } if elapsed := time.Since(started); elapsed > 2*time.Second { t.Fatalf("Run() timeout took %s", elapsed) } } func TestValidateRequestRejectsOptionAndShellLikeTargets(t *testing.T) { for _, target := range []string{"-c", "host;reboot", "$(id)", "host name"} { request := Request{Kind: KindNetworkTest, Target: target, TimeoutSeconds: 5, MaxOutputBytes: 1024} if err := ValidateRequest(request); err == nil { t.Fatalf("ValidateRequest() accepted %q", target) } } } func TestProcessRunnerTCPConnectionTestConnectsToRequestedPort(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer listener.Close() port := listener.Addr().(*net.TCPAddr).Port runner, err := NewProcessRunner(ProcessRunnerOptions{PingPath: "/bin/ping", TraceroutePath: "/bin/traceroute"}) if err != nil { t.Fatal(err) } result, err := runner.Run(context.Background(), Request{ Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 2, MaxOutputBytes: 1024, }) if err != nil { t.Fatalf("Run() error = %v", err) } if result.Port != port || result.Reachable == nil || !*result.Reachable || result.ExitCode != 0 || result.OutputTruncated { t.Fatalf("result = %+v", result) } operationResult := result.OperationResult(2, 1024) if operationResult["adapter"] != "socket" || operationResult["port"] != port { t.Fatalf("operation result = %#v", operationResult) } } func TestValidateRequestRequiresTCPPort(t *testing.T) { for _, port := range []int{0, 65536} { request := Request{Kind: KindTCPConnectionTest, Target: "127.0.0.1", Port: port, TimeoutSeconds: 5, MaxOutputBytes: 1024} if err := ValidateRequest(request); err == nil { t.Fatalf("ValidateRequest() accepted TCP port %d", port) } } } func TestOSExecutorBoundsCombinedOutputAndReturnsExitCode(t *testing.T) { if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "output" { fmt.Print(strings.Repeat("x", 4096)) os.Exit(3) } t.Setenv("TERA_DIAGNOSTICS_HELPER", "output") execution, err := (OSExecutor{}).Execute(context.Background(), os.Args[0], []string{"-test.run=TestOSExecutorBoundsCombinedOutputAndReturnsExitCode"}, 1024) if err != nil { t.Fatalf("Execute() error = %v", err) } if execution.ExitCode != 3 || execution.Bytes != 1024 || len(execution.Output) != 1024 || !execution.Truncated { t.Fatalf("execution = %+v", execution) } } func TestOSExecutorReportsLiveOutput(t *testing.T) { if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "stream" { fmt.Print("first reply\n") time.Sleep(50 * time.Millisecond) fmt.Print("second reply\n") return } t.Setenv("TERA_DIAGNOSTICS_HELPER", "stream") var mu sync.Mutex reports := make([]OutputProgress, 0, 2) ctx := WithOutputReporter(context.Background(), func(progress OutputProgress) { mu.Lock() defer mu.Unlock() reports = append(reports, progress) }) execution, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorReportsLiveOutput"}, 1024) if err != nil { t.Fatal(err) } mu.Lock() defer mu.Unlock() if len(reports) == 0 || !strings.Contains(reports[len(reports)-1].Output, "first reply") || !strings.Contains(reports[len(reports)-1].Output, "second reply") { t.Fatalf("live reports = %#v", reports) } if execution.Output != reports[len(reports)-1].Output { t.Fatalf("final output = %q, last live output = %q", execution.Output, reports[len(reports)-1].Output) } } func TestOSExecutorHonorsContextTimeout(t *testing.T) { if os.Getenv("TERA_DIAGNOSTICS_HELPER") == "sleep" { time.Sleep(5 * time.Second) os.Exit(0) } t.Setenv("TERA_DIAGNOSTICS_HELPER", "sleep") ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() _, err := (OSExecutor{}).Execute(ctx, os.Args[0], []string{"-test.run=TestOSExecutorHonorsContextTimeout"}, 1024) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Execute() error = %v, want deadline exceeded", err) } }