package fullsyncretry import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" ) func TestReplayAndStatusProxy(t *testing.T) { calls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if r.Header.Get("Authorization") != "" { t.Error("Admin JWT must not be forwarded to Worker") } if r.Method == "POST" { var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Error(err) } if len(body) != 1 || body["requestedBy"] != "42" { t.Errorf("unexpected Worker request: %+v", body) } w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{"replayRunId":"replay-1","status":"accepted"}`)) return } _, _ = w.Write([]byte(`{"replayRunId":"replay-1","sourceRunId":"source","status":"completed","remaining":0,"enqueued":3}`)) })) defer server.Close() client := Client{BaseURL: server.URL} r, err := client.Replay(context.Background(), "source", "42", 0) if err != nil || r.ReplayRunID != "replay-1" { t.Fatalf("replay: %+v %v", r, err) } s, err := client.Get(context.Background(), "replay-1") if err != nil || s.Enqueued != 3 || calls != 2 { t.Fatalf("status: %+v %v, calls=%d", s, err, calls) } } func TestRunQueriesAndFailures(t *testing.T) { calls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ switch { case r.Method == http.MethodGet && r.URL.Path == "/internal/v1/full-sync-runs": if r.URL.Query().Get("syncKind") != "traffic_usage" || r.URL.Query().Get("limit") != "7" { t.Fatalf("query = %s", r.URL.RawQuery) } _, _ = w.Write([]byte(`{"items":[{"source":"live","run":{"runId":"run-1","operation":"current_month_usage_full_sync","status":"running"}}],"total":1}`)) case r.Method == http.MethodGet && r.URL.Path == "/internal/v1/full-sync-runs/run-1": _, _ = w.Write([]byte(`{"source":"archive","run":{"runId":"run-1","operation":"current_month_usage_full_sync","status":"completed"},"rounds":[]}`)) case r.Method == http.MethodGet && r.URL.Path == "/internal/v1/full-sync-runs/run-1/failures": _, _ = w.Write([]byte(`{"items":[{"taskId":"task-1","cardId":1,"errorCategory":"business","errorMessage":"failed","round":2}],"nextCursor":"next"}`)) default: http.NotFound(w, r) } })) defer server.Close() client := Client{BaseURL: server.URL} list, err := client.ListRuns(context.Background(), "traffic_usage", 7) if err != nil || list.Total != 1 || len(list.Items) != 1 { t.Fatalf("list = %+v, err = %v", list, err) } view, err := client.Run(context.Background(), "run-1") if err != nil || view.Source != "archive" { t.Fatalf("run = %+v, err = %v", view, err) } page, err := client.Failures(context.Background(), "run-1", 100, "cursor") if err != nil || len(page.Items) != 1 || page.NextCursor != "next" || calls != 3 { t.Fatalf("failures = %+v, err = %v, calls = %d", page, err, calls) } } func TestReplayWorkerTimeout(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(50 * time.Millisecond) })) defer server.Close() client := Client{BaseURL: server.URL, HTTP: &http.Client{Timeout: time.Millisecond}} _, err := client.Replay(context.Background(), "source", "42", 0) if err == nil || !strings.Contains(err.Error(), "Worker unavailable") { t.Fatalf("timeout: %v", err) } } func TestReplayValidationAndWorkerErrors(t *testing.T) { calls := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ w.WriteHeader(http.StatusConflict) _, _ = w.Write([]byte(`{"error":{"message":"ICCID secret"}}`)) })) defer server.Close() client := Client{BaseURL: server.URL} for _, id := range []string{"", "a/b", "{bad}", strings.Repeat("a", 129)} { if _, err := client.Replay(context.Background(), id, "42", 0); gerror.Code(err) != gcode.CodeInvalidParameter { t.Fatalf("id=%q: %v", id, err) } } if calls != 0 { t.Fatal("invalid source triggered Worker") } // The edge must reject an oversized round budget before calling Worker. for _, rounds := range []int{-1, MaxReplayRounds + 1} { if _, err := client.Replay(context.Background(), "source", "42", rounds); gerror.Code(err) != gcode.CodeInvalidParameter { t.Fatalf("maxRounds=%d: %v", rounds, err) } } if calls != 0 { t.Fatal("invalid maxRounds triggered Worker") } _, err := client.Replay(context.Background(), "source", "42", 0) if gerror.Code(err) != gcode.CodeInvalidParameter || strings.Contains(err.Error(), "ICCID") { t.Fatalf("error leaked Worker body: %v", err) } for _, status := range []int{400, 401, 404, 409, 422, 500} { if workerError(status) == nil { t.Fatalf("missing error mapping for %d", status) } } } // The round budget must reach Worker only when the caller asks for one, so an // omitted value keeps the Worker-side configured default authoritative. func TestReplayForwardsMaxRoundsOnlyWhenSet(t *testing.T) { var bodies []map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body map[string]any _ = json.NewDecoder(r.Body).Decode(&body) bodies = append(bodies, body) w.WriteHeader(http.StatusAccepted) _, _ = w.Write([]byte(`{"replayRunId":"replay-1","status":"accepted"}`)) })) defer server.Close() client := Client{BaseURL: server.URL} if _, err := client.Replay(context.Background(), "source", "42", 0); err != nil { t.Fatal(err) } if _, err := client.Replay(context.Background(), "source", "42", 7); err != nil { t.Fatal(err) } if len(bodies) != 2 { t.Fatalf("expected 2 Worker calls, got %d", len(bodies)) } if _, present := bodies[0]["maxRounds"]; present { t.Fatalf("omitted budget must not be sent: %+v", bodies[0]) } if got := bodies[1]["maxRounds"]; got != float64(7) { t.Fatalf("explicit budget must be forwarded, got %+v", bodies[1]) } }