package httpapi import ( "context" "errors" "knowledge-governance/internal/dobotaiops" "net/http" "net/http/httptest" "strings" "testing" ) type qaOpsSpy struct { query dobotaiops.RecordQuery tenant, id string calls int err error } func (s *qaOpsSpy) ListRecords(_ context.Context, q dobotaiops.RecordQuery) (dobotaiops.RecordPage, error) { s.query = q s.calls++ return dobotaiops.RecordPage{Items: []dobotaiops.Record{}}, s.err } func (s *qaOpsSpy) GetRecord(_ context.Context, tenant, id string) (dobotaiops.Record, error) { s.tenant = tenant s.id = id s.calls++ return dobotaiops.Record{}, s.err } func (s *qaOpsSpy) Available(context.Context) error { return s.err } func (s *qaOpsSpy) Safety() dobotaiops.SourceSafety { return dobotaiops.SourceSafety{DatabaseTemp: true, SSL: false} } func TestQAOperationsRequireExplicitSessionAndServerSelectedTenant(t *testing.T) { spy := &qaOpsSpy{} auth := newFakeAuthStore() for token, user := range map[string]string{"admin-qa-fixture": "usr-boss", "viewer-qa-fixture": "usr-eye", "engineer-qa-fixture": "usr-eng"} { auth.sessions[token] = user } handler, err := NewHandler(Config{Store: &fakeJobStore{}, QAOps: spy, QAOpsEnabled: true, 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 _, tc := range []struct { token, path string status int }{ {"", base, http.StatusUnauthorized}, {"viewer-qa-fixture", base, http.StatusForbidden}, {"engineer-qa-fixture", base, http.StatusForbidden}, {"admin-qa-fixture", base + "&tenant_key=another-tenant", http.StatusBadRequest}, {"admin-qa-fixture", base + "&limit=0", http.StatusBadRequest}, {"admin-qa-fixture", base + "&keyword=a&keyword=b", http.StatusBadRequest}, {"admin-qa-fixture", base + "&keyword=test", http.StatusOK}, } { request := httptest.NewRequest(http.MethodGet, tc.path, nil) if tc.token != "" { request.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tc.token}) } response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != tc.status { t.Fatalf("status=%d want=%d body=%s", response.Code, tc.status, response.Body.String()) } } if spy.calls != 1 || spy.query.Window.TenantKey != "approved-tenant" || spy.query.Keyword != "test" { t.Fatalf("unauthorized/invalid reads reached source: %+v", spy) } spy.err = errors.New("sensitive-driver-detail-not-for-browser") request := httptest.NewRequest(http.MethodGet, base, nil) request.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "admin-qa-fixture"}) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusServiceUnavailable || strings.Contains(response.Body.String(), "sensitive-driver-detail") { t.Fatalf("source failure leaked or became empty success: %s", response.Body.String()) } if response.Header().Get("Cache-Control") != "no-store" { t.Fatal("employee answers must not be cached") } unauth, _ := NewHandler(Config{Store: &fakeJobStore{}, QAOps: spy, QAOpsTenantKey: "approved-tenant"}) response = httptest.NewRecorder() unauth.ServeHTTP(response, httptest.NewRequest(http.MethodGet, base, nil)) if response.Code != http.StatusUnauthorized { t.Fatal("auth-disabled governance must not expose production answers") } }