| index 3607ebc8e..98e014010 100644 |
| --- a/e2e_test.go |
| +++ b/e2e_test.go |
| @@ -358,10 +358,29 @@ func TestEndToEndSmoke(t *testing.T) { |
| if code != 200 { |
| t.Fatalf("GET status.local /fleet.json = %d, want 200", code) |
| } |
| + // DisallowUnknownFields turns this decode into a schema check too: a |
| + // stray top-level key (one Fleet doesn't declare) fails the test instead |
| + // of silently vanishing into an unused field. |
| var decodedFleet status.Fleet |
| - if err := json.Unmarshal([]byte(body), &decodedFleet); err != nil { |
| + dec := json.NewDecoder(strings.NewReader(body)) |
| + dec.DisallowUnknownFields() |
| + if err := dec.Decode(&decodedFleet); err != nil { |
| t.Fatalf("decode status.local /fleet.json: %v\nbody: %s", err, body) |
| } |
| + if decodedFleet.GeneratedAt.IsZero() { |
| + t.Errorf("fleet.json generated_at is zero, want the publish tick's timestamp") |
| + } |
| + wantLabel := status.Label("e2e-box") // deterministic from the fixture MachineID above |
| + if len(decodedFleet.Boxes) != 1 || decodedFleet.Boxes[0].Label != wantLabel { |
| + t.Errorf("fleet.json boxes = %+v, want exactly one box labeled %q", decodedFleet.Boxes, wantLabel) |
| + } |
| + // Every route CollectBox saw falls into exactly one of static/resident/ |
| + // hibernating, so instances.total is always the route count — app.local |
| + // (service), site.local (static), status.local (status): 3 routes, one box. |
| + wantTotal := len(statusRoutes) |
| + if got := decodedFleet.Instances["total"]; got == nil || *got != wantTotal { |
| + t.Errorf("fleet.json instances.total = %v, want %d (one box over %d routes)", got, wantTotal, wantTotal) |
| + } |
| |
| // Binary channel pointer flow: ship v2 (same bytes — a rebuild), promote, |
| // then roll back; the pointer must return to v1 with v2 as the rollback |
| @@ -389,14 +408,46 @@ func TestEndToEndSmoke(t *testing.T) { |
| } |
| } |
| |
| -// TestStatusDryRun exercises the CLI's `carlos status -dry-run <dir>` path. |
| -// RunStatus (cli.go) is three lines of flag parsing around status.DryRun — |
| -// the substance under test is DryRun itself, run the way an operator would |
| -// run it against a scratch directory before ever pointing the binary at a |
| -// real bucket: build a fixture fleet stuffed with needles, publish it, read |
| -// every object back, and fail loudly if anything leaked. |
| -func TestStatusDryRun(t *testing.T) { |
| - if err := status.DryRun(t.TempDir(), io.Discard); err != nil { |
| - t.Fatalf("status.DryRun: %v", err) |
| +// TestStatusCLI exercises the actual `carlos status` command line, not just |
| +// the function underneath it: builds the real carlos binary the same way |
| +// this file already builds testdata/echoapp, then runs it as a subprocess |
| +// exactly the way an operator would from a shell. |
| +// |
| +// - `carlos status -dry-run <dir>` must exit 0 and its stdout must carry |
| +// DryRun's "dry-run: no leaks" attestation — proving cli.go's RunStatus |
| +// is wired to internal/status.DryRun correctly. DryRun's own substance |
| +// (the fixture-needle leak scan) is already exercised directly by |
| +// TestDryRunSelfChecks in internal/status/status_test.go; duplicating |
| +// that here would just be the same assertion via a slower path, so this |
| +// test's job is only the wrapper's wiring and exit code. |
| +// - a bare `carlos status` (no -dry-run) must exit non-zero — RunStatus's |
| +// own input validation, not the pipeline it wraps. |
| +func TestStatusCLI(t *testing.T) { |
| + goBin, err := exec.LookPath("go") |
| + if err != nil { |
| + t.Skip("go toolchain not found; skipping status CLI test") |
| + } |
| + tmp := t.TempDir() |
| + carlosBin := filepath.Join(tmp, "carlos") |
| + build := exec.Command(goBin, "build", "-o", carlosBin, ".") |
| + build.Env = os.Environ() |
| + if out, err := build.CombinedOutput(); err != nil { |
| + t.Fatalf("build carlos: %v\n%s", err, out) |
| + } |
| + |
| + dryRunDir := filepath.Join(tmp, "dryrun") |
| + if err := os.MkdirAll(dryRunDir, 0o755); err != nil { |
| + t.Fatalf("mkdir dry-run dir: %v", err) |
| + } |
| + out, err := exec.Command(carlosBin, "status", "-dry-run", dryRunDir).CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("carlos status -dry-run %s: %v\n%s", dryRunDir, err, out) |
| + } |
| + if !strings.Contains(string(out), "dry-run: no leaks") { |
| + t.Errorf("carlos status -dry-run output = %q, want it to contain %q", out, "dry-run: no leaks") |
| + } |
| + |
| + if err := exec.Command(carlosBin, "status").Run(); err == nil { |
| + t.Error("carlos status with no -dry-run flag: want a non-zero exit, got 0") |
| } |
| } |