feat: bootstrap supply intelligence baseline
This commit is contained in:
266
internal/httpapi/server_test.go
Normal file
266
internal/httpapi/server_test.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"supply-intelligence/internal/discovery"
|
||||
"supply-intelligence/internal/domain"
|
||||
"supply-intelligence/internal/gatewayconsumer"
|
||||
"supply-intelligence/internal/probe"
|
||||
"supply-intelligence/internal/publish"
|
||||
"supply-intelligence/internal/repository"
|
||||
)
|
||||
|
||||
func TestServerRoutingStateEndpoint(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
repo.UpsertRoutingState(domain.AccountRoutingState{
|
||||
AccountID: 101,
|
||||
Platform: "openai",
|
||||
AccountStatus: domain.AccountStatusActive,
|
||||
RoutingEnabled: true,
|
||||
RiskScore: 10,
|
||||
ReasonCode: "ok",
|
||||
LastProbeAt: time.Unix(100, 0).UTC(),
|
||||
Version: 3,
|
||||
})
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/accounts/101/routing-state", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var got domain.AccountRoutingState
|
||||
if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if got.AccountID != 101 || got.AccountStatus != domain.AccountStatusActive {
|
||||
t.Fatalf("unexpected payload: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerProbeEvaluateEndpointPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantStatus int
|
||||
wantClassification domain.ProbeClassification
|
||||
wantAccountStatus domain.AccountStatus
|
||||
wantReasonCode string
|
||||
wantRoutingEnabled bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
body: `{"account_id":201,"platform":"openai","current_status":"suspended","status_code":200}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantClassification: domain.ProbeClassificationSuccess,
|
||||
wantAccountStatus: domain.AccountStatusActive,
|
||||
wantReasonCode: "ok",
|
||||
wantRoutingEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "explicit_failure",
|
||||
body: `{"account_id":202,"platform":"openai","current_status":"active","status_code":401}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantClassification: domain.ProbeClassificationExplicitFailure,
|
||||
wantAccountStatus: domain.AccountStatusSuspended,
|
||||
wantReasonCode: "auth_rejected",
|
||||
wantRoutingEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "inconclusive",
|
||||
body: `{"account_id":203,"platform":"openai","current_status":"suspended","transport_error":"dial tcp timeout"}`,
|
||||
wantStatus: http.StatusOK,
|
||||
wantClassification: domain.ProbeClassificationInconclusive,
|
||||
wantAccountStatus: domain.AccountStatusSuspended,
|
||||
wantReasonCode: "transport_error",
|
||||
wantRoutingEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/probe/evaluate", bytes.NewBufferString(tt.body))
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
if rr.Code != tt.wantStatus {
|
||||
t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var got probe.EvaluateOutput
|
||||
if err := json.NewDecoder(rr.Body).Decode(&got); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if got.Classification != tt.wantClassification {
|
||||
t.Fatalf("unexpected classification: %q", got.Classification)
|
||||
}
|
||||
if got.RoutingState.AccountStatus != tt.wantAccountStatus {
|
||||
t.Fatalf("unexpected account status: %q", got.RoutingState.AccountStatus)
|
||||
}
|
||||
if got.RoutingState.ReasonCode != tt.wantReasonCode {
|
||||
t.Fatalf("unexpected reason code: %q", got.RoutingState.ReasonCode)
|
||||
}
|
||||
if got.RoutingState.RoutingEnabled != tt.wantRoutingEnabled {
|
||||
t.Fatalf("unexpected routing enabled: %v", got.RoutingState.RoutingEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerPublishPackageEventEndpoint(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
body := bytes.NewBufferString(`{"event_id":"evt-1","package_id":1001,"platform":"openai","model":"gpt-4.1-mini","version":7,"occurred_at":"2026-05-06T20:30:00Z"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/publish/package-event", body)
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected publish status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var event domain.PackageChangeEvent
|
||||
if err := json.NewDecoder(rr.Body).Decode(&event); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if event.EventID != "evt-1" || event.EventType != publish.PackagePublishedEventType {
|
||||
t.Fatalf("unexpected event: %+v", event)
|
||||
}
|
||||
if event.GatewaySyncStatus != domain.GatewaySyncStatusPending {
|
||||
t.Fatalf("unexpected sync status: %q", event.GatewaySyncStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerPackageChangeListAndAck(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-1", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending})
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/gateway/package-changes", nil)
|
||||
listRR := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(listRR, listReq)
|
||||
if listRR.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected list status: %d body=%s", listRR.Code, listRR.Body.String())
|
||||
}
|
||||
var listResp struct {
|
||||
Items []domain.PackageChangeEvent `json:"items"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
}
|
||||
if err := json.NewDecoder(listRR.Body).Decode(&listResp); err != nil {
|
||||
t.Fatalf("decode list error: %v", err)
|
||||
}
|
||||
if len(listResp.Items) != 1 || listResp.NextCursor != "1" {
|
||||
t.Fatalf("unexpected list response: %+v", listResp)
|
||||
}
|
||||
|
||||
ackReq := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/gateway/package-changes/evt-1/ack", bytes.NewBufferString(`{"consumer":"gateway","result":"applied","detail":"ok"}`))
|
||||
ackRR := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(ackRR, ackReq)
|
||||
if ackRR.Code != http.StatusNoContent {
|
||||
t.Fatalf("unexpected ack status: %d body=%s", ackRR.Code, ackRR.Body.String())
|
||||
}
|
||||
updated, _ := repo.ListPackageEventsAfter("")
|
||||
if len(updated) != 1 || updated[0].GatewaySyncStatus != domain.GatewaySyncStatusApplied {
|
||||
t.Fatalf("unexpected ack state: %+v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerPackageChangeListWithCursor(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-1", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending})
|
||||
repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-2", EventType: publish.PackagePublishedEventType, PackageID: 1002, Platform: "openai", Model: "gpt-4.1", OccurredAt: time.Unix(6, 0).UTC(), Version: 8, GatewaySyncStatus: domain.GatewaySyncStatusPending})
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/gateway/package-changes?cursor=1", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
Items []domain.PackageChangeEvent `json:"items"`
|
||||
NextCursor string `json:"next_cursor"`
|
||||
}
|
||||
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if len(resp.Items) != 1 || resp.Items[0].EventID != "evt-2" || resp.NextCursor != "2" {
|
||||
t.Fatalf("unexpected cursor response: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConsumeOnceEndpoint(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-apply", EventType: publish.PackagePublishedEventType, PackageID: 1001, Platform: "openai", Model: "gpt-4.1-mini", OccurredAt: time.Unix(5, 0).UTC(), Version: 7, GatewaySyncStatus: domain.GatewaySyncStatusPending})
|
||||
repo.AppendPackageEvent(domain.PackageChangeEvent{EventID: "evt-fail", EventType: publish.PackagePublishedEventType, PackageID: 1002, Platform: "openai", Model: "gpt-fail-model", OccurredAt: time.Unix(6, 0).UTC(), Version: 8, GatewaySyncStatus: domain.GatewaySyncStatusPending})
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/gateway/consume-once", bytes.NewBufferString(`{"consumer":"gateway"}`))
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected consume status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
var out gatewayconsumer.ConsumeOnceOutput
|
||||
if err := json.NewDecoder(rr.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
if len(out.Items) != 2 {
|
||||
t.Fatalf("unexpected consume output length: %+v", out)
|
||||
}
|
||||
if out.Items[0].Result != domain.GatewayAckResultApplied || out.Items[0].GatewaySyncStatus != domain.GatewaySyncStatusApplied || out.Items[0].Detail == "" {
|
||||
t.Fatalf("unexpected first consume item: %+v", out.Items[0])
|
||||
}
|
||||
if out.Items[1].Result != domain.GatewayAckResultFailed || out.Items[1].GatewaySyncStatus != domain.GatewaySyncStatusFailed || out.Items[1].Detail == "" {
|
||||
t.Fatalf("unexpected second consume item: %+v", out.Items[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerDiscoveryCandidateCreateAndList(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
createReq := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/discovery/candidates", bytes.NewBufferString(`{"candidate_id":"cand-1","account_id":301,"platform":"openai","model":"gpt-4.1-mini","source":"manual_seed","reason_code":"new_model","discovered_at":"2026-05-06T20:30:00Z"}`))
|
||||
createRR := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(createRR, createReq)
|
||||
if createRR.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected create status: %d body=%s", createRR.Code, createRR.Body.String())
|
||||
}
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/internal/supply-intelligence/discovery/candidates?status=pending_admission", nil)
|
||||
listRR := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(listRR, listReq)
|
||||
if listRR.Code != http.StatusOK {
|
||||
t.Fatalf("unexpected list status: %d body=%s", listRR.Code, listRR.Body.String())
|
||||
}
|
||||
var listResp struct {
|
||||
Items []domain.DiscoveryCandidate `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(listRR.Body).Decode(&listResp); err != nil {
|
||||
t.Fatalf("decode list error: %v", err)
|
||||
}
|
||||
if len(listResp.Items) != 1 || listResp.Items[0].CandidateID != "cand-1" || listResp.Items[0].Status != domain.DiscoveryCandidateStatusPendingAdmission {
|
||||
t.Fatalf("unexpected discovery list response: %+v", listResp.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerDiscoveryCandidateRejectsInvalidInput(t *testing.T) {
|
||||
repo := repository.NewMemoryRepository()
|
||||
server := NewServer(repo, probe.NewService(repo), publish.NewService(repo), gatewayconsumer.NewService(repo), discovery.NewService(repo), nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/internal/supply-intelligence/discovery/candidates", bytes.NewBufferString(`{"candidate_id":"","account_id":0}`))
|
||||
rr := httptest.NewRecorder()
|
||||
server.Routes().ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("unexpected status: %d body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user