package httpapi import ( "context" "encoding/json" "knowledge-governance/internal/dobotaiops" "net/http" "net/http/httptest" "strings" "testing" "unicode/utf8" ) type qaPreviewSpy struct { qaOpsSpy record dobotaiops.Record } func (s *qaPreviewSpy) ListRecords(_ context.Context, q dobotaiops.RecordQuery) (dobotaiops.RecordPage, error) { s.query = q return dobotaiops.RecordPage{Items: []dobotaiops.Record{s.record}, Total: 1}, nil } func TestQALightweightListPreservesFullRecordAndRejectsUnknownProjection(t *testing.T) { full := strings.Repeat("长😀", 900) spy := &qaPreviewSpy{record: dobotaiops.Record{ID: "preview-fixture", Question: full, Answer: full, MasterFields: map[string]string{"question_chinese": full, "product_category": "MG400"}, References: json.RawMessage(`[{"content":"private-reference-body"}]`), Media: json.RawMessage(`[{"url":"private-media"}]`)}} auth := newFakeAuthStore() auth.sessions["admin-qa-fixture"] = "usr-boss" h, err := NewHandler(Config{Store: &fakeJobStore{}, QAOps: spy, QAOpsTenantKey: "approved-tenant", Auth: &AuthConfig{Store: auth}}) if err != nil { t.Fatal(err) } base := "/api/v1/qa-ops/records?start=2026-10-14T00:00:00Z&end=2026-10-15T00:00:00Z" for _, mode := range []string{"summary", "full", "invalid"} { req := httptest.NewRequest("GET", base+"&projection="+mode, nil) req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "admin-qa-fixture"}) out := httptest.NewRecorder() h.ServeHTTP(out, req) if mode == "invalid" { if out.Code != 400 { t.Fatal("unknown projection accepted") } continue } if out.Code != 200 { t.Fatalf("projection %s status %d", mode, out.Code) } var data struct{ Data dobotaiops.RecordPage } if json.Unmarshal(out.Body.Bytes(), &data) != nil || len(data.Data.Items) != 1 { t.Fatal("invalid page") } row := data.Data.Items[0] if mode == "summary" { if utf8.RuneCountInString(row.Answer) > 401 || row.Answer == full || strings.Contains(out.Body.String(), "private-reference-body") || strings.Contains(out.Body.String(), "private-media") { t.Fatal("summary leaked full content") } } else if row.Answer != full || !strings.Contains(out.Body.String(), "private-reference-body") { t.Fatal("full export content changed") } } if spy.record.Answer != full || spy.record.MasterFields["question_chinese"] != full { t.Fatal("preview mutated original") } _ = http.StatusOK } func TestQARecordsAcceptFiveExactFiltersAndRejectUnsupportedValues(t *testing.T) { spy := &qaOpsSpy{} auth := newFakeAuthStore() auth.sessions["admin-qa-fixture"] = "usr-boss" h, err := NewHandler(Config{Store: &fakeJobStore{}, QAOps: spy, QAOpsTenantKey: "approved-tenant", Auth: &AuthConfig{Store: auth}}) if err != nil { t.Fatal(err) } base := "/api/v1/qa-ops/records?start=2026-10-14T00:00:00Z&end=2026-10-15T00:00:00Z" for _, suffix := range []string{"&coverage=是&is_correct=否&product_category=MG400&problem_type=参数&root_cause=知识缺失", "&root_cause=__empty__"} { req := httptest.NewRequest("GET", base+suffix, nil) req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "admin-qa-fixture"}) out := httptest.NewRecorder() h.ServeHTTP(out, req) if out.Code != 200 || len(spy.query.Filters) == 0 { t.Fatalf("filter not forwarded: %d", out.Code) } } for _, suffix := range []string{"&coverage=invalid", "&is_correct=是&is_correct=否", "&product_category=" + strings.Repeat("x", 501)} { req := httptest.NewRequest("GET", base+suffix, nil) req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "admin-qa-fixture"}) out := httptest.NewRecorder() h.ServeHTTP(out, req) if out.Code != 400 { t.Fatal("invalid filters accepted") } } }