88 lines
3.3 KiB
Go
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")
|
|
}
|
|
}
|