package tide import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestParityRoundTrip(t *testing.T) { ctx := context.Background() specPath := filepath.Join("testdata", "one-route-spec.yaml") spec, err := LoadFlow(specPath) if err != nil { t.Fatal(err) } t.Run("json record replay and scalar mismatch", func(t *testing.T) { orig := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/sample" || r.Method != http.MethodGet { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"data":"ok"}`)) })) t.Cleanup(orig.Close) changed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"data":"no"}`)) })) t.Cleanup(changed.Close) recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: orig.URL}) if err != nil { t.Fatal(err) } out := filepath.Join(t.TempDir(), "sample.yaml") if err := SaveFlow(out, recorded); err != nil { t.Fatal(err) } raw, err := os.ReadFile(out) if err != nil { t.Fatal(err) } text := string(raw) if !strings.Contains(text, "version: 1") { t.Fatalf("missing version:\n%s", text) } if !strings.Contains(text, "body: |") && !strings.Contains(text, "body: |-") { t.Fatalf("body is not a literal block scalar:\n%s", text) } if !strings.Contains(text, `{"data":"ok"}`) { t.Fatalf("missing recorded JSON body:\n%s", text) } if !strings.Contains(text, "application/json") { t.Fatalf("missing Content-Type:\n%s", text) } loaded, err := LoadFlow(out) if err != nil { t.Fatal(err) } if _, err := ReplayFlow(ctx, loaded, ReplayConfig{Target: orig.URL}); err != nil { t.Fatalf("identical backend must replay: %v", err) } _, err = ReplayFlow(ctx, loaded, ReplayConfig{Target: changed.URL}) if err == nil { t.Fatal("changed JSON must fail") } msg := err.Error() if !strings.Contains(msg, "$.data") { t.Fatalf("mismatch missing $.data: %s", msg) } if !strings.Contains(msg, "ok") || !strings.Contains(msg, "no") { t.Fatalf("mismatch missing expected/actual: %s", msg) } }) t.Run("non-json byte offset mismatch", func(t *testing.T) { orig := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("hello")) })) t.Cleanup(orig.Close) changed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("hallo")) })) t.Cleanup(changed.Close) recorded, err := RecordFlow(ctx, spec, RecordConfig{Target: orig.URL}) if err != nil { t.Fatal(err) } if _, err := ReplayFlow(ctx, recorded, ReplayConfig{Target: orig.URL}); err != nil { t.Fatalf("identical plain body must replay: %v", err) } _, err = ReplayFlow(ctx, recorded, ReplayConfig{Target: changed.URL}) if err == nil { t.Fatal("changed bytes must fail") } msg := err.Error() if !strings.Contains(msg, "1") { t.Fatalf("byte mismatch missing offset: %s", msg) } }) }