# Step 6 — Integration Tests ## What this step delivers A full integration-test suite covering every success and failure mode of the `latprobe` CLI. Tests are written using the Go standard library only (`testing`, `net/http/httptest`, `os/exec`, `encoding/json`) — no third-party dependencies. Two test layers: | Layer | File | How it runs | |-------|------|-------------| | In-process | `go/run_test.go` | Calls `run(args, stdout, stderr)` directly; fast, covers the full matrix | | Subprocess | `go/cli_test.go` | Builds the real binary once, `exec.Command`s it; exercises the true `os.Exit` path | ## Running the tests ```sh cd go # Run all tests (quiet) go test ./... # Run with per-case output go test -v ./... # Run only in-process tests go test -v -run TestRunMatrix . go test -v -run TestJSON . # Run only subprocess smoke tests go test -v -run TestCLI . ``` ## Test matrix ### In-process (`run_test.go`) | Test case | Exit code | Assertion | |-----------|-----------|-----------| | Success 200 | 0 | stdout contains `(200)`, `TCP connect`, `Total` | | HTTP 500, no `--fail` | 0 | stdout contains `(500)`, `Total` | | HTTP 404, `--fail` | 6 | stdout contains `404 ✗` | | DNS failure (`.invalid` TLD) | 2 | stdout contains `✗ dns:` | | Connection refused (listen-then-close) | 3 | stdout contains `✗ connect:` | | Timeout (`--timeout 200ms`, blocking handler) | 4 | stdout contains `✗ timeout:` | | TLS failure (self-signed cert) | 5 | stdout contains `✗ tls:` | | Multiple URLs (200 + `.invalid`) | 2 (highest) | stdout contains both `(200)` and `✗ dns:` | | Sampling `-n 3`, all success | 0 | stdout contains `3 samples`, `min`, `avg`, `max` | | No args | 1 | stderr contains `Usage:` | | `-h` | 0 | stderr contains `Usage:` | | `--json` success | 0 | valid JSON, `phases.total` present, `failed == 0` | | `--json` DNS failure | 2 | valid JSON, `errors[0].phase == "dns"`, `succeeded == 0` | | `--json` `-n 3` success | 0 | `succeeded == 3`, `total.min_ms > 0`, `max_ms >= min_ms` | ### Subprocess smoke tests (`cli_test.go`) `TestMain` builds the binary with `go build -o /latprobe .` once before any test runs. The binary is deleted on test completion. | Test | Checks | |------|--------| | `TestCLISuccess` | Local server, exit 0, stdout has `(200)` and `Total` | | `TestCLIDNSFailure` | `.invalid` host, real binary exits 2 | | `TestCLIJSONDNSFailure` | `.invalid` host, JSON output, `errors[0].phase == "dns"` | ## Notes - The `http: TLS handshake error` log line printed during the TLS test is the **server-side** log of the client correctly rejecting the self-signed cert. It is expected and harmless. - `httptest.NewServer` binds to `127.0.0.1`; Go resolves loopback addresses without a DNS query, so the DNS row does not appear in localhost test output. Tests use `TCP connect` as the success-path phase assertion instead. - The test suite requires no network access for any case except DNS failure, which uses the reserved `.invalid` TLD (RFC 6761 — always NXDOMAIN). ## Code changes included in this step Beyond the tests, two code changes were made: 1. **`main.go` refactor** — extracted `run(args []string, stdout, stderr io.Writer) int` so the CLI is testable in-process. `main()` is now a one-liner: `os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))`. No behaviour change. 2. **`probe.go` TLS classification fix** — Go's `httptrace` calls `TLSHandshakeDone` with the error on a failed handshake, so `tlsDone` was set even on cert rejection. The previous classifier ("tlsStart set, tlsDone zero") never matched. Fixed by capturing `tlsErr` from the hook and checking it in `classifyErr`.