package httpapi import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/catalog" "github.com/descrybe/descrybe-v2/apps/api/internal/processing" "github.com/go-chi/chi/v5" "github.com/google/uuid" ) func TestV1OKDataMetaEnvelope(t *testing.T) { t.Parallel() rec := httptest.NewRecorder() v1OK(rec, http.StatusOK, []map[string]any{{"id": "1"}}, map[string]any{ "page": 1, "limit": 25, "total": 1, "totalPages": 1, }) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if _, ok := body["data"]; !ok { t.Fatalf("missing data: %s", rec.Body.String()) } meta, ok := body["meta"].(map[string]any) if !ok { t.Fatalf("missing meta: %s", rec.Body.String()) } if meta["page"].(float64) != 1 || meta["limit"].(float64) != 25 { t.Fatalf("meta=%v", meta) } } func TestV1OKOmitsNilMeta(t *testing.T) { t.Parallel() rec := httptest.NewRecorder() v1OK(rec, http.StatusOK, map[string]any{"process_id": "abc"}, nil) var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if _, ok := body["meta"]; ok { t.Fatalf("meta should be omitted: %s", rec.Body.String()) } data, _ := body["data"].(map[string]any) if data["process_id"] != "abc" { t.Fatalf("data=%v", data) } } func TestV1ErrCodedEnvelope(t *testing.T) { t.Parallel() rec := httptest.NewRecorder() v1Err(rec, http.StatusBadRequest, "validation_error", "'items' array is required") var body struct { Error struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Error.Code != "validation_error" || !strings.Contains(body.Error.Message, "items") { t.Fatalf("got %+v", body.Error) } } func TestV1ErrFromProcessingDoesNotDoubleWrite(t *testing.T) { t.Parallel() rec := httptest.NewRecorder() v1ErrFromProcessing(rec, errors.New("unexpected backend failure")) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d", rec.Code) } raw := rec.Body.Bytes() var body struct { Error struct { Code string `json:"code"` Message string `json:"message"` } `json:"error"` } if err := json.Unmarshal(raw, &body); err != nil { t.Fatalf("body must be a single JSON object: %v raw=%q", err, string(raw)) } if body.Error.Code != "validation_error" { t.Fatalf("got %+v", body.Error) } if strings.Contains(string(raw), `{"error":"could not start processing job"}`) { t.Fatalf("flat Error envelope must not precede coded body: %s", raw) } } func TestHandleV1StartProcessRequiresItemsOrRawIDs(t *testing.T) { t.Parallel() s := &Server{} cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uuid.MustParse("22222222-2222-2222-2222-222222222222")) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(`{"processing_type":"full"}`)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), `"code":"validation_error"`) { t.Fatalf("body=%s", rec.Body.String()) } if !strings.Contains(rec.Body.String(), "items") { t.Fatalf("body=%s", rec.Body.String()) } } func TestHandleV1StartProcessRejectsMissingEAN(t *testing.T) { t.Parallel() s := &Server{} cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uuid.MustParse("22222222-2222-2222-2222-222222222222")) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(`{"items":[{"title":"x"}]}`)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "ean") { t.Fatalf("body=%s", rec.Body.String()) } } func TestHandleV1StartProcessItemsEANReturnsProcessID(t *testing.T) { t.Parallel() jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") rawID := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb") cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") uid := uuid.MustParse("22222222-2222-2222-2222-222222222222") var sawEANs []string var enqueued uuid.UUID s := &Server{ testEnsureRawV1Items: func(_ context.Context, companyID uuid.UUID, items []catalog.V1ProcessItem) ([]uuid.UUID, []catalog.EnsureRawResult, []string, error) { if companyID != cid { t.Fatalf("company=%s", companyID) } for _, it := range items { sawEANs = append(sawEANs, it.EAN) } return []uuid.UUID{rawID}, []catalog.EnsureRawResult{{RawProductID: rawID, EAN: items[0].EAN}}, nil, nil }, testStartJobs: func(_ context.Context, companyID, userID uuid.UUID, rawIDs []uuid.UUID, processingType string) ([]processing.Job, error) { if companyID != cid || userID != uid { t.Fatalf("tenant cid=%s uid=%s", companyID, userID) } if len(rawIDs) != 1 || rawIDs[0] != rawID { t.Fatalf("rawIDs=%v", rawIDs) } if processingType != "full" { t.Fatalf("type=%q", processingType) } return []processing.Job{{ID: jobID, Status: "pending", TotalProducts: 1, ProcessingType: "full"}}, nil }, testEnqueueJob: func(_ context.Context, id uuid.UUID) error { enqueued = id return nil }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uid) body := `{"processing_type":"full","items":[{"ean":"1234567890123","title":"Wireless earbuds"}]}` rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(body)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var envelope struct { Data struct { ProcessID string `json:"process_id"` Status string `json:"status"` Message string `json:"message"` TotalItems int `json:"total_items"` ProcessedItems int `json:"processed_items"` } `json:"data"` } if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil { t.Fatalf("json: %v body=%s", err, rec.Body.String()) } if envelope.Data.ProcessID != jobID.String() { t.Fatalf("process_id=%q want %s", envelope.Data.ProcessID, jobID) } if envelope.Data.Status != "pending" { t.Fatalf("status=%q want pending", envelope.Data.Status) } if envelope.Data.TotalItems != 1 || envelope.Data.ProcessedItems != 0 { t.Fatalf("counts=%+v (processed_items must be 0 on start)", envelope.Data) } if enqueued != jobID { t.Fatalf("enqueued=%s", enqueued) } if len(sawEANs) != 1 || sawEANs[0] != "1234567890123" { t.Fatalf("eans=%v", sawEANs) } if strings.Contains(rec.Body.String(), `"meta"`) { t.Fatalf("start response should omit meta: %s", rec.Body.String()) } } func TestHandleV1StartProcessUnauthorizedWithoutCompany(t *testing.T) { t.Parallel() s := &Server{} rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(`{"items":[{"ean":"1"}]}`)) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("status=%d", rec.Code) } if !strings.Contains(rec.Body.String(), `"code":"unauthorized"`) { t.Fatalf("body=%s", rec.Body.String()) } } func TestRouterV1ProductsProcessAuthUsesCodedError(t *testing.T) { t.Parallel() s := testAPIServer() h := s.Router() rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(`{"items":[{"ean":"1"}]}`))) if rec.Code != http.StatusUnauthorized { t.Fatalf("status=%d", rec.Code) } if !strings.Contains(rec.Body.String(), `"code":"unauthorized"`) || !strings.Contains(rec.Body.String(), `"message":"Unauthorized"`) { t.Fatalf("want coded envelope, got %s", rec.Body.String()) } } func TestHandleV1StartProcessRejectsInvalidProcessingTypes(t *testing.T) { t.Parallel() s := &Server{} cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uuid.MustParse("22222222-2222-2222-2222-222222222222")) rec := httptest.NewRecorder() body := `{"processing_types":["both"],"items":[{"ean":"1234567890123"}]}` req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(body)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "validation_error") { t.Fatalf("body=%s", rec.Body.String()) } } func TestHandleV1StartProcessRejectsTooManyRawIDs(t *testing.T) { // Not parallel: mutates process-wide SetTestStartProductCap. processing.SetTestStartProductCap(2) t.Cleanup(func() { processing.SetTestStartProductCap(0) }) s := &Server{ testEnsureRawV1Items: func(context.Context, uuid.UUID, []catalog.V1ProcessItem) ([]uuid.UUID, []catalog.EnsureRawResult, []string, error) { t.Fatal("ensureRaw must not run for oversized payloads") return nil, nil, nil, nil }, testStartJobs: func(context.Context, uuid.UUID, uuid.UUID, []uuid.UUID, string) ([]processing.Job, error) { t.Fatal("startJobs must not run for oversized payloads") return nil, nil }, } cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uuid.MustParse("22222222-2222-2222-2222-222222222222")) body := fmt.Sprintf( `{"processing_type":"full","raw_product_ids":[%q,%q,%q]}`, uuid.New().String(), uuid.New().String(), uuid.New().String(), ) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(body)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "too many products") { t.Fatalf("body=%s", rec.Body.String()) } } func TestHandleV1GetProcessPassesCompanyScope(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") var sawCompany, sawJob uuid.UUID s := &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { sawCompany = companyID sawJob = id return processing.Job{}, errors.New("not found") }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/products/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) req = req.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleV1GetProcess(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if sawCompany != cid || sawJob != jobID { t.Fatalf("scoped call company=%s job=%s", sawCompany, sawJob) } } func TestHandleV1GetProcessCompletedIncludesProcessedItems(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { if companyID != cid || id != jobID { t.Fatalf("scoped call company=%s job=%s", companyID, id) } return processing.Job{ ID: jobID, CompanyID: cid, Status: "completed", ProcessingType: "full", TotalProducts: 1, }, nil }, testLoadV1ProcessJobItems: func(_ context.Context, companyID, id uuid.UUID, processingType string) ([]processing.V1ProcessJobItem, error) { if companyID != cid || id != jobID || processingType != "full" { t.Fatalf("load scope company=%s job=%s type=%s", companyID, id, processingType) } return []processing.V1ProcessJobItem{{ "ean": "0123456789012", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "status": "processed", "title": "Acme Widget", }}, nil }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/products/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) req = req.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleV1GetProcess(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var body struct { Data map[string]any `json:"data"` } if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Data["status"] != "COMPLETED" { t.Fatalf("status=%v", body.Data["status"]) } if body.Data["total_items"].(float64) != 1 { t.Fatalf("total_items=%v", body.Data["total_items"]) } items, ok := body.Data["items"].([]any) if !ok || len(items) != 1 { t.Fatalf("items=%v", body.Data["items"]) } item, ok := items[0].(map[string]any) if !ok { t.Fatalf("item=%T", items[0]) } if item["status"] != "processed" || item["ean"] != "0123456789012" { t.Fatalf("item=%v", item) } if _, ok := item["id"]; ok { t.Fatalf("id must be omitted from public process items: %v", item) } if _, ok := body.Data["processed_at"]; !ok { t.Fatalf("missing processed_at: %v", body.Data) } } func TestHandleGetProcessingJobCompletedIncludesItems(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { return processing.Job{ ID: jobID, CompanyID: companyID, Status: "completed", ProcessingType: "full", TotalProducts: 1, }, nil }, testLoadV1ProcessJobItems: func(context.Context, uuid.UUID, uuid.UUID, string) ([]processing.V1ProcessJobItem, error) { return []processing.V1ProcessJobItem{{ "ean": "1", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", "status": "processed", "title": "T", }}, nil }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) req = req.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleGetProcessingJob(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["status"] != "completed" { t.Fatalf("status=%v", body["status"]) } if body["total_items"].(float64) != 1 { t.Fatalf("total_items=%v", body["total_items"]) } items, ok := body["items"].([]any) if !ok || len(items) != 1 { t.Fatalf("items=%v", body["items"]) } } func TestHandleV1GetProcessJobOmitsAIEnhance(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") note := "GPT: rewrite title with --- Title --- markers" errMsg := "openai: invalid api-key sk-live-secret" s := &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { return processing.Job{ ID: jobID, CompanyID: companyID, Status: "processing", ProcessingType: "full", CurrentStep: "ai_enhance", StepProgress: []processing.StepProgress{{ Step: "ai_enhance", Status: "running", Note: note, }}, Error: &errMsg, TotalProducts: 2, ProcessedProducts: 1, }, nil }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) req = req.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleV1GetProcessJob(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } raw := rec.Body.String() if strings.Contains(raw, "ai_enhance") { t.Fatalf("public process poll leaked ai_enhance: %s", raw) } if strings.Contains(raw, "GPT:") || strings.Contains(raw, "--- Title ---") { t.Fatalf("public process poll leaked step notes: %s", raw) } if strings.Contains(raw, "current_step") || strings.Contains(raw, "step_progress") || strings.Contains(raw, "company_id") { t.Fatalf("public process poll leaked internals: %s", raw) } var body map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["status"] != "processing" { t.Fatalf("status=%v", body["status"]) } if body["total_products"].(float64) != 2 || body["processed_products"].(float64) != 1 { t.Fatalf("counts=%v", body) } } func TestHandleGetProcessingJobKeepsCurrentStep(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { return processing.Job{ ID: jobID, CompanyID: companyID, Status: "processing", CurrentStep: "ai_enhance", }, nil }, } ctx := context.WithValue(context.Background(), ctxCompanyID, cid) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/processing/jobs/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) req = req.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleGetProcessingJob(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "ai_enhance") { t.Fatalf("dashboard GET should keep current_step: %s", rec.Body.String()) } } func TestHandleV1ListProcessJobsUnauthorizedWithoutCompany(t *testing.T) { t.Parallel() s := &Server{} rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/process", nil) s.handleV1ListProcessJobs(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } } func TestHandleV1StartProcessGatesBeforeEnsureRaw(t *testing.T) { t.Parallel() ensureCalled := false s := &Server{ Billing: &billing.Service{}, // Pool nil → entitlements lookup fails before EnsureRaw testEnsureRawV1Items: func(context.Context, uuid.UUID, []catalog.V1ProcessItem) ([]uuid.UUID, []catalog.EnsureRawResult, []string, error) { ensureCalled = true return nil, nil, nil, nil }, } cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") ctx := context.WithValue(context.Background(), ctxCompanyID, cid) ctx = context.WithValue(ctx, ctxUserID, uuid.MustParse("22222222-2222-2222-2222-222222222222")) body := `{"processing_type":"enhance","items":[{"ean":"1234567890123","title":"x"}]}` rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/products/process", strings.NewReader(body)) req = req.WithContext(ctx) s.handleV1StartProcess(rec, req) if ensureCalled { t.Fatal("EnsureRaw must not run when billing gate fails") } if rec.Code == http.StatusOK || rec.Code == http.StatusCreated || rec.Code == http.StatusAccepted { t.Fatalf("expected gate failure, got %d %s", rec.Code, rec.Body.String()) } var env map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil { t.Fatalf("response json: %v body=%s", err, rec.Body.String()) } if _, ok := env["error"]; !ok { if _, ok := env["code"]; !ok { t.Fatalf("expected error envelope, got %s", rec.Body.String()) } } } func TestAssertV1ProcessGatesNilBilling(t *testing.T) { t.Parallel() s := &Server{} if err := s.assertV1ProcessGates(context.Background(), uuid.New(), "enhance", 2); err != nil { t.Fatalf("nil billing must no-op: %v", err) } } func processJobWithSEOMetaItems(cid, jobID uuid.UUID) *Server { return &Server{ testGetJob: func(_ context.Context, companyID, id uuid.UUID) (processing.Job, error) { return processing.Job{ ID: jobID, CompanyID: companyID, Status: "completed", ProcessingType: "full", TotalProducts: 1, }, nil }, testLoadV1ProcessJobItems: func(context.Context, uuid.UUID, uuid.UUID, string) ([]processing.V1ProcessJobItem, error) { return []processing.V1ProcessJobItem{{ "ean": "0123456789012", "status": "processed", "name": "Acme Widget", "meta_title": "Acme | Widget", "meta_description": "A widget for tests", }}, nil }, } } func getProcessJSONItem(t *testing.T, rec *httptest.ResponseRecorder, wrapped bool) map[string]any { t.Helper() if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } var raw map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil { t.Fatal(err) } data := raw if wrapped { var ok bool data, ok = raw["data"].(map[string]any) if !ok { t.Fatalf("data=%T body=%s", raw["data"], rec.Body.String()) } } items, ok := data["items"].([]any) if !ok || len(items) != 1 { t.Fatalf("items=%v", data["items"]) } item, ok := items[0].(map[string]any) if !ok { t.Fatalf("item=%T", items[0]) } return item } func TestA1ProcessEndpointsOmitSEOMeta(t *testing.T) { t.Parallel() cid := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := processJobWithSEOMetaItems(cid, jobID) ctx := context.WithValue(context.Background(), ctxCompanyID, cid) legacyRec := httptest.NewRecorder() legacyReq := httptest.NewRequest(http.MethodGet, "/api/v1/products/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) legacyReq = legacyReq.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleV1GetProcess(legacyRec, legacyReq) legacyItem := getProcessJSONItem(t, legacyRec, true) if _, ok := legacyItem["meta_title"]; ok { t.Fatalf("GET /products/process/{id} A1 meta_title=%v", legacyItem["meta_title"]) } if _, ok := legacyItem["meta_description"]; ok { t.Fatalf("GET /products/process/{id} A1 meta_description=%v", legacyItem["meta_description"]) } jobRec := httptest.NewRecorder() jobReq := httptest.NewRequest(http.MethodGet, "/api/v1/process/"+jobID.String(), nil) jrctx := chi.NewRouteContext() jrctx.URLParams.Add("id", jobID.String()) jobReq = jobReq.WithContext(context.WithValue(ctx, chi.RouteCtxKey, jrctx)) s.handleGetProcessingJob(jobRec, jobReq) jobItem := getProcessJSONItem(t, jobRec, false) if _, ok := jobItem["meta_title"]; ok { t.Fatalf("GET /process/{id} A1 meta_title=%v", jobItem["meta_title"]) } if _, ok := jobItem["meta_description"]; ok { t.Fatalf("GET /process/{id} A1 meta_description=%v", jobItem["meta_description"]) } } func TestNonA1ProcessEndpointsKeepSEOMeta(t *testing.T) { t.Parallel() cid := uuid.MustParse("11111111-1111-1111-1111-111111111111") jobID := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa") s := processJobWithSEOMetaItems(cid, jobID) ctx := context.WithValue(context.Background(), ctxCompanyID, cid) legacyRec := httptest.NewRecorder() legacyReq := httptest.NewRequest(http.MethodGet, "/api/v1/products/process/"+jobID.String(), nil) rctx := chi.NewRouteContext() rctx.URLParams.Add("id", jobID.String()) legacyReq = legacyReq.WithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx)) s.handleV1GetProcess(legacyRec, legacyReq) legacyItem := getProcessJSONItem(t, legacyRec, true) if legacyItem["meta_title"] != "Acme | Widget" { t.Fatalf("non-A1 GET /products/process/{id} meta_title=%v", legacyItem["meta_title"]) } if legacyItem["meta_description"] != "A widget for tests" { t.Fatalf("non-A1 GET /products/process/{id} meta_description=%v", legacyItem["meta_description"]) } jobRec := httptest.NewRecorder() jobReq := httptest.NewRequest(http.MethodGet, "/api/v1/process/"+jobID.String(), nil) jrctx := chi.NewRouteContext() jrctx.URLParams.Add("id", jobID.String()) jobReq = jobReq.WithContext(context.WithValue(ctx, chi.RouteCtxKey, jrctx)) s.handleGetProcessingJob(jobRec, jobReq) jobItem := getProcessJSONItem(t, jobRec, false) if jobItem["meta_title"] != "Acme | Widget" { t.Fatalf("non-A1 GET /process/{id} meta_title=%v", jobItem["meta_title"]) } if jobItem["meta_description"] != "A widget for tests" { t.Fatalf("non-A1 GET /process/{id} meta_description=%v", jobItem["meta_description"]) } }