Files
knowledge-governance/internal/httpapi/qa_ops_auth_test.go
T

88 lines
3.3 KiB
Go

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")
}
}