3e7fcce0b1
build / build (push) Successful in 1m11s
新增迁移50与每5分钟/手动持久同步,API只读本地;同步失败保留、权限阻断、租约与质量失效原子激活。 保留全量稳定分页/固定列,已存质量批量读取,元数据不反复解压长答案;详情安全Markdown与原文。 已运行整仓 make verify、源与副本隔离PG -race、真实只读源及隔离浏览器。 实际同步2525条;固定窗口640条本地中位1.46秒,对照直接源33.10秒。 停worker、缺源凭据、重启API后仍可查询;未更新8088,未修改生产原问答。
87 lines
3.4 KiB
Go
87 lines
3.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"knowledge-governance/internal/qaopsmirror"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type mirrorSyncSpy struct {
|
|
qaOpsSpy
|
|
syncCalls int
|
|
key, actor string
|
|
}
|
|
|
|
func (s *mirrorSyncSpy) GetSyncStatus(context.Context) (qaopsmirror.Status, error) {
|
|
return qaopsmirror.Status{Mode: "local", IntervalSeconds: 300, WorkerReady: true}, nil
|
|
}
|
|
func (s *mirrorSyncSpy) EnqueueSync(_ context.Context, key, actor string) (qaopsmirror.SyncReceipt, bool, error) {
|
|
s.syncCalls++
|
|
s.key = key
|
|
s.actor = actor
|
|
return qaopsmirror.SyncReceipt{JobID: "fixture-mirror-job"}, false, nil
|
|
}
|
|
func TestQAMirrorSyncRequiresSessionIntentAndIdempotency(t *testing.T) {
|
|
spy := &mirrorSyncSpy{}
|
|
auth := newFakeAuthStore()
|
|
auth.sessions["mirror-admin"] = "usr-boss"
|
|
auth.sessions["mirror-viewer"] = "usr-eye"
|
|
h, e := NewHandler(Config{Store: &fakeJobStore{}, QAOps: spy, QAOpsEnabled: true, QAOpsTenantKey: "fixture-tenant", Auth: &AuthConfig{Store: auth}})
|
|
if e != nil {
|
|
t.Fatal(e)
|
|
}
|
|
for _, tc := range []struct {
|
|
method, token, key, intent, origin, body string
|
|
status int
|
|
}{
|
|
{"GET", "", "", "", "", "", 401}, {"GET", "mirror-viewer", "", "", "", "", 403}, {"GET", "mirror-admin", "", "", "", "", 200},
|
|
{"POST", "mirror-admin", "op-one", "", "", "{}", 403}, {"POST", "mirror-admin", "op-one", "qa-records-sync", "https://foreign.invalid", "{}", 403},
|
|
{"POST", "mirror-admin", "", "qa-records-sync", "", "{}", 400}, {"POST", "mirror-admin", "op-one", "qa-records-sync", "", "null", 400},
|
|
{"POST", "mirror-admin", "op-one", "qa-records-sync", "", `{"tenant":"forged"}`, 400}, {"POST", "mirror-admin", "op-one", "qa-records-sync", "", "{}", 202},
|
|
} {
|
|
req := httptest.NewRequest(tc.method, "/api/v1/qa-ops/sync", strings.NewReader(tc.body))
|
|
if tc.token != "" {
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tc.token})
|
|
}
|
|
req.Header.Set("Idempotency-Key", tc.key)
|
|
req.Header.Set("X-Governance-Action", tc.intent)
|
|
req.Header.Set("Origin", tc.origin)
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != tc.status {
|
|
t.Fatalf("%s %s got %d want %d: %s", tc.method, tc.body, w.Code, tc.status, w.Body.String())
|
|
}
|
|
}
|
|
if spy.syncCalls != 1 || spy.key != "op-one" || spy.actor == "" {
|
|
t.Fatal("invalid sync reached store or operator was not session-stamped")
|
|
}
|
|
req := httptest.NewRequest("GET", "/api/v1/qa-ops/status", nil)
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "mirror-admin"})
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
var body struct {
|
|
Data struct {
|
|
Cache *qaopsmirror.Status `json:"cache"`
|
|
}
|
|
}
|
|
if json.Unmarshal(w.Body.Bytes(), &body) != nil || body.Data.Cache == nil || body.Data.Cache.Mode != "local" {
|
|
t.Fatal("local serving mode missing from status")
|
|
}
|
|
}
|
|
func TestQAMirrorSyncNamespaceCannotBypassEntry(t *testing.T) {
|
|
auth := newFakeAuthStore()
|
|
auth.sessions["mirror-admin"] = "usr-boss"
|
|
h, _ := NewHandler(Config{Store: &fakeJobStore{}, Auth: &AuthConfig{Store: auth}})
|
|
req := httptest.NewRequest("POST", "/api/v1/jobs", strings.NewReader(`{"type":"qaops.records.sync.fixture","payload":{},"idempotency_key":"bypass"}`))
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "mirror-admin"})
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
if w.Code != 400 {
|
|
t.Fatal("generic jobs endpoint bypassed mirror sync admission", w.Code)
|
|
}
|
|
}
|