86 lines
3.2 KiB
Go
86 lines
3.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"knowledge-governance/internal/qaopsorg"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type orgSyncSpy struct {
|
|
calls int
|
|
key, actor string
|
|
replay bool
|
|
err error
|
|
}
|
|
|
|
func (s *orgSyncSpy) GetSyncStatus(context.Context) (qaopsorg.SyncStatus, error) {
|
|
s.calls++
|
|
return qaopsorg.SyncStatus{Configured: true, WorkerReady: true}, s.err
|
|
}
|
|
func (s *orgSyncSpy) EnqueueSync(_ context.Context, key, actor string) (qaopsorg.SyncReceipt, bool, error) {
|
|
s.calls++
|
|
s.key = key
|
|
s.actor = actor
|
|
return qaopsorg.SyncReceipt{JobID: "fixture-job"}, s.replay, s.err
|
|
}
|
|
func TestQAOrganizationSyncRequiresAdminIntentAndDurableOperation(t *testing.T) {
|
|
spy := &orgSyncSpy{}
|
|
auth := newFakeAuthStore()
|
|
auth.sessions["sync-admin"] = "usr-boss"
|
|
auth.sessions["sync-viewer"] = "usr-eye"
|
|
handler, err := NewHandler(Config{Store: &fakeJobStore{}, QAOrgSync: spy, QAOrgSyncEnabled: true, Auth: &AuthConfig{Store: auth}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
method, token, key, action, origin, body string
|
|
status int
|
|
}{
|
|
{"GET", "", "", "", "", "", 401}, {"GET", "sync-viewer", "", "", "", "", 403}, {"GET", "sync-admin", "", "", "", "", 200},
|
|
{"POST", "sync-admin", "op-one", "", "", "{}", 403}, {"POST", "sync-admin", "op-one", "qa-organization", "https://evil.invalid", "{}", 403},
|
|
{"POST", "sync-admin", "", "qa-organization", "", "{}", 400}, {"POST", "sync-admin", " op-one ", "qa-organization", "", "{}", 400},
|
|
{"POST", "sync-admin", "op-one", "qa-organization", "", `null`, 400},
|
|
{"POST", "sync-admin", "op-one", "qa-organization", "", `{"tenant":"forged"}`, 400}, {"POST", "sync-admin", "op-one", "qa-organization", "", "{} {}", 400},
|
|
{"POST", "sync-admin", "op-one", "qa-organization", "", "{}", 202},
|
|
} {
|
|
req := httptest.NewRequest(tc.method, "/api/v1/qa-ops/organization/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.action)
|
|
req.Header.Set("Origin", tc.origin)
|
|
res := httptest.NewRecorder()
|
|
handler.ServeHTTP(res, req)
|
|
if res.Code != tc.status {
|
|
t.Fatalf("%+v got %d: %s", tc, res.Code, res.Body.String())
|
|
}
|
|
}
|
|
if spy.calls != 2 || spy.key != "op-one" || spy.actor == "" {
|
|
t.Fatalf("invalid request reached store: %+v", spy)
|
|
}
|
|
spy.replay = true
|
|
req := httptest.NewRequest("POST", "/api/v1/qa-ops/organization/sync", strings.NewReader("{}"))
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "sync-admin"})
|
|
req.Header.Set("Idempotency-Key", "op-one")
|
|
req.Header.Set("X-Governance-Action", "qa-organization")
|
|
res := httptest.NewRecorder()
|
|
handler.ServeHTTP(res, req)
|
|
var body map[string]any
|
|
if json.Unmarshal(res.Body.Bytes(), &body) != nil || body["operation"] != "replayed" {
|
|
t.Fatal("operation replay not exposed")
|
|
}
|
|
disabled, _ := NewHandler(Config{Store: &fakeJobStore{}, QAOrgSync: spy, Auth: &AuthConfig{Store: auth}})
|
|
req.Body = io.NopCloser(strings.NewReader("{}"))
|
|
res = httptest.NewRecorder()
|
|
disabled.ServeHTTP(res, req)
|
|
if res.Code != 503 {
|
|
t.Fatal("disabled sync enqueued")
|
|
}
|
|
}
|