package tide import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" ) func TestManifestContract(t *testing.T) { ctx := context.Background() fixtures := t.TempDir() if err := os.MkdirAll(filepath.Join(fixtures, "routes"), 0o755); err != nil { t.Fatal(err) } var hits atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/pass": _, _ = w.Write([]byte(`{"v":1}`)) case "/fail": _, _ = w.Write([]byte(`{"v":2}`)) default: _, _ = w.Write([]byte(`{"v":0}`)) } })) t.Cleanup(srv.Close) writeRouteFixture(t, srv.URL, fixtures, "pass", "/pass", `{"v":1}`) writeRouteFixture(t, srv.URL, fixtures, "fail", "/fail", `{"v":1}`) m := Manifest{ Version: 1, AuthGroups: []string{"public"}, Routes: []Route{ routeEntry("GET /pass public", "/pass", StatusPorted, "routes/pass.yaml"), routeEntry("GET /fail public", "/fail", StatusPorted, "routes/fail.yaml"), {ID: "GET /none public", Method: http.MethodGet, Path: "/none", AuthGroup: "public", Status: StatusPending}, }, } before := hits.Load() cov, err := ReplayManifest(ctx, m, ManifestConfig{Target: srv.URL, Fixtures: fixtures, Mode: ModeRequireRecorded}) if err == nil { t.Fatal("ported fail plus unrecorded required must be nonzero") } if cov.Passing != 1 || cov.Failing != 1 || cov.Unrecorded != 1 || cov.Recorded != 2 { t.Fatalf("coverage %+v", cov) } if cov.Pending != 1 || cov.Ported != 2 { t.Fatalf("pending/ported counts %+v", cov) } if !strings.Contains(cov.SummaryLine(), "recorded 2/3") { t.Fatalf("summary %s", cov.SummaryLine()) } if hits.Load()-before != 2 { t.Fatalf("comparison failure must still replay later recorded flows, extra hits=%d", hits.Load()-before) } foundFailPath := false for _, d := range cov.Diffs { if strings.Contains(d, "GET /fail public") && (strings.Contains(d, "$.v") || strings.Contains(d, ".v")) { foundFailPath = true } } if !foundFailPath { t.Fatalf("coverage diffs missing path for failing route: %v", cov.Diffs) } t.Run("pending mismatch is measured not fatal", func(t *testing.T) { pending := Manifest{ Version: 1, AuthGroups: []string{"public"}, Routes: []Route{routeEntry("GET /fail public", "/fail", StatusPending, "routes/fail.yaml")}, } cov, err := ReplayManifest(ctx, pending, ManifestConfig{Target: srv.URL, Fixtures: fixtures}) if err != nil { t.Fatalf("pending mismatch must not fail: %v", err) } if cov.Failing != 1 || cov.Passing != 0 { t.Fatalf("pending failing %+v", cov) } if _, err := ReplayManifest(ctx, pending, ManifestConfig{Target: srv.URL, Fixtures: fixtures, SelfCheck: true}); err == nil { t.Fatal("self-check must fail pending mismatches") } }) t.Run("require-recorded reports unrecorded", func(t *testing.T) { empty := Manifest{ Version: 1, AuthGroups: []string{"public"}, Routes: []Route{{ ID: "GET /ghost public", Method: http.MethodGet, Path: "/ghost", AuthGroup: "public", Status: StatusPending, }}, } if err := ValidateManifest(empty, fixtures, ModeAllowIncomplete); err != nil { t.Fatal(err) } if err := ValidateManifest(empty, fixtures, ModeRequireRecorded); err == nil { t.Fatal("missing cases must fail require-recorded") } cov, err := ReplayManifest(ctx, empty, ManifestConfig{Target: srv.URL, Fixtures: fixtures, Mode: ModeRequireRecorded}) if err == nil { t.Fatal("unrecorded required must error") } if cov.Unrecorded != 1 || cov.Passing != 0 { t.Fatalf("unrecorded coverage %+v", cov) } }) }