131 lines
5.8 KiB
Go
131 lines
5.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"knowledge-governance/internal/dobotaiops"
|
|
"knowledge-governance/internal/qaopsorg"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type attributionOrgSpy struct {
|
|
orgSpy
|
|
projection *qaopsorg.Attribution
|
|
}
|
|
|
|
func (s *attributionOrgSpy) GetAttribution(context.Context) (*qaopsorg.Attribution, error) {
|
|
return s.projection, s.err
|
|
}
|
|
|
|
type attributedReader struct{}
|
|
|
|
func (*attributedReader) Available(context.Context) error { return nil }
|
|
func (*attributedReader) Safety() dobotaiops.SourceSafety { return dobotaiops.SourceSafety{SSL: true} }
|
|
func (*attributedReader) ListRecords(context.Context, dobotaiops.RecordQuery) (dobotaiops.RecordPage, error) {
|
|
return dobotaiops.RecordPage{Items: []dobotaiops.Record{{ID: "anonymous"}}, ScopeActors: []dobotaiops.ActorIdentity{{StaffID: "fixture-private-staff"}}, Columns: []string{"question"}, Total: 3}, nil
|
|
}
|
|
func (*attributedReader) GetRecord(context.Context, string, string) (dobotaiops.Record, error) {
|
|
return dobotaiops.Record{ID: "one", Department: "历史部门", Region: "历史区域", Identity: dobotaiops.ActorIdentity{StaffID: "fixture-private-staff"}}, nil
|
|
}
|
|
func (*attributedReader) ReadUsageGroups(context.Context, dobotaiops.RecordQuery) (dobotaiops.UsageGroups, error) {
|
|
return dobotaiops.UsageGroups{AsOf: time.Date(2026, 10, 15, 0, 0, 0, 0, time.UTC), Groups: []dobotaiops.UsageGroup{{Identity: dobotaiops.ActorIdentity{StaffID: "fixture-private-staff"}, Total: 3, Succeeded: 2, Failed: 1}, {Identity: dobotaiops.ActorIdentity{UnionID: "fixture-private-union"}, Total: 2, Succeeded: 2}, {Identity: dobotaiops.ActorIdentity{StaffID: "unmatched"}, Total: 2}}}, nil
|
|
}
|
|
func attributionHandler(t *testing.T) (http.Handler, *attributionOrgSpy) {
|
|
t.Helper()
|
|
projection, err := qaopsorg.BuildAttribution(qaopsorg.State{State: "ready", DirectoryID: "directory-v1", RevisionID: "region-v1", Rules: []qaopsorg.Rule{{DepartmentID: "root", Region: "APAC"}}}, qaopsorg.Directory{Departments: []qaopsorg.Department{{ID: "root", Name: "测试公司"}}, People: []qaopsorg.Person{{ID: "fixture-private-staff", UnionID: "fixture-private-union", DepartmentIDs: []string{"root"}}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
org := &attributionOrgSpy{projection: projection}
|
|
auth := newFakeAuthStore()
|
|
auth.sessions["attribution-admin"] = "usr-boss"
|
|
auth.sessions["attribution-viewer"] = "usr-eye"
|
|
h, err := NewHandler(Config{Store: &fakeJobStore{}, Auth: &AuthConfig{Store: auth}, QAOps: &attributedReader{}, QAOpsTenantKey: "fixed", QAOrg: org, Now: func() time.Time { return time.Date(2026, 10, 17, 0, 0, 0, 0, time.UTC) }})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return h, org
|
|
}
|
|
func attributionRequest(h http.Handler, path, token string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest("GET", path, nil)
|
|
r.AddCookie(&http.Cookie{Name: sessionCookieName, Value: token})
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
|
|
const attributionFilter = "?start=2026-10-14T00:00:00Z&end=2026-10-15T00:00:00Z"
|
|
|
|
func TestQAAttributionUsesWholeFilterAndPreservesHistoricalSnapshots(t *testing.T) {
|
|
h, org := attributionHandler(t)
|
|
w := attributionRequest(h, "/api/v1/qa-ops/records"+attributionFilter, "attribution-admin")
|
|
if w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body.String())
|
|
}
|
|
var page struct{ Data dobotaiops.RecordPage }
|
|
_ = json.Unmarshal(w.Body.Bytes(), &page)
|
|
if !strings.Contains(strings.Join(page.Data.Columns, ","), "department,region") {
|
|
t.Fatal("organization column hidden because first page is anonymous", w.Body.String())
|
|
}
|
|
w = attributionRequest(h, "/api/v1/qa-ops/records/one", "attribution-admin")
|
|
var detail struct{ Data dobotaiops.Record }
|
|
_ = json.Unmarshal(w.Body.Bytes(), &detail)
|
|
if detail.Data.Department != "历史部门" || detail.Data.Region != "历史区域" || detail.Data.Organization == nil || detail.Data.Organization.Region != "APAC" {
|
|
t.Fatal("source snapshot overwritten or current attribution missing", w.Body.String())
|
|
}
|
|
if strings.Contains(w.Body.String(), "fixture-private") {
|
|
t.Fatal("identity keys exposed")
|
|
}
|
|
org.err = qaopsorg.ErrUnavailable
|
|
w = attributionRequest(h, "/api/v1/qa-ops/records"+attributionFilter, "attribution-admin")
|
|
if w.Code != 503 || !strings.Contains(w.Body.String(), "organization_unavailable") {
|
|
t.Fatal("permission/storage failure hidden as empty", w.Body.String())
|
|
}
|
|
}
|
|
func TestQAUsageIsAdminScopedDeduplicatedAndNotPerformance(t *testing.T) {
|
|
h, _ := attributionHandler(t)
|
|
path := "/api/v1/qa-ops/usage" + attributionFilter
|
|
for _, tc := range []struct {
|
|
token string
|
|
status int
|
|
}{{"", 401}, {"attribution-viewer", 403}, {"attribution-admin", 200}} {
|
|
w := attributionRequest(h, path, tc.token)
|
|
if w.Code != tc.status {
|
|
t.Fatalf("usage guard got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if tc.status == 200 {
|
|
var result struct {
|
|
Data struct {
|
|
Questions int64 `json:"questions"`
|
|
IdentifiedUsers int `json:"identified_users"`
|
|
Rows []struct {
|
|
Key string
|
|
Questions int64
|
|
Users *int
|
|
}
|
|
}
|
|
}
|
|
if json.Unmarshal(w.Body.Bytes(), &result) != nil || result.Data.Questions != 7 || result.Data.IdentifiedUsers != 1 {
|
|
t.Fatal("events/people not deduplicated", w.Body.String())
|
|
}
|
|
for _, row := range result.Data.Rows {
|
|
if row.Key == "APAC" && (row.Questions != 5 || row.Users == nil || *row.Users != 1) {
|
|
t.Fatal("regional user double-counted")
|
|
}
|
|
}
|
|
if strings.Contains(w.Body.String(), "fixture-private") || strings.Contains(w.Body.String(), "performance") {
|
|
t.Fatal("private identity or performance verdict leaked")
|
|
}
|
|
}
|
|
}
|
|
for _, extra := range []string{"&tenant=forged", "&limit=1", "&cursor=forged", "&start=duplicate"} {
|
|
if w := attributionRequest(h, path+extra, "attribution-admin"); w.Code != 400 {
|
|
t.Fatal("usage accepted client scope or pagination")
|
|
}
|
|
}
|
|
}
|