579 lines
20 KiB
Go
579 lines
20 KiB
Go
//go:build integration
|
|
|
|
// Package integration contains Sprint 11 E2E tests (E11-01b).
|
|
// Batch 2 covers: admin policies CRUD, feature flags admin API, audit log
|
|
// query, cost aggregation, compliance entry CRUD, and GDPR erasure.
|
|
//
|
|
// Run with: go test -tags integration -v ./test/integration/ -run TestE2E_Admin
|
|
// All tests use httptest.Server + in-memory stubs — no external services needed.
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/veylant/ia-gateway/internal/admin"
|
|
"github.com/veylant/ia-gateway/internal/auditlog"
|
|
"github.com/veylant/ia-gateway/internal/compliance"
|
|
"github.com/veylant/ia-gateway/internal/flags"
|
|
"github.com/veylant/ia-gateway/internal/middleware"
|
|
"github.com/veylant/ia-gateway/internal/routing"
|
|
)
|
|
|
|
// ─── Admin server builder ─────────────────────────────────────────────────────
|
|
|
|
type adminServerOptions struct {
|
|
store routing.RuleStore
|
|
auditLog auditlog.Logger
|
|
flagStore flags.FlagStore
|
|
verifier middleware.TokenVerifier
|
|
// compStore: if nil, a memComplianceStore is created automatically
|
|
compStore compliance.ComplianceStore
|
|
}
|
|
|
|
// buildAdminServer creates a fully wired httptest.Server exposing /v1/admin/*
|
|
// and /v1/admin/compliance/* with in-memory stores. No external services needed.
|
|
func buildAdminServer(t *testing.T, opts adminServerOptions) *httptest.Server {
|
|
t.Helper()
|
|
|
|
if opts.store == nil {
|
|
opts.store = routing.NewMemStore()
|
|
}
|
|
if opts.auditLog == nil {
|
|
opts.auditLog = auditlog.NewMemLogger()
|
|
}
|
|
if opts.flagStore == nil {
|
|
opts.flagStore = flags.NewMemFlagStore()
|
|
}
|
|
if opts.verifier == nil {
|
|
opts.verifier = &middleware.MockVerifier{Claims: e2eAdminClaims()}
|
|
}
|
|
if opts.compStore == nil {
|
|
opts.compStore = newMemComplianceStore()
|
|
}
|
|
|
|
cache := routing.NewRuleCache(opts.store, 5*time.Minute, zap.NewNop())
|
|
adminHandler := admin.NewWithAudit(opts.store, cache, opts.auditLog, zap.NewNop()).
|
|
WithFlagStore(opts.flagStore)
|
|
|
|
compHandler := compliance.New(opts.compStore, zap.NewNop()).
|
|
WithAudit(opts.auditLog)
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(chimiddleware.Recoverer)
|
|
|
|
r.Route("/v1", func(r chi.Router) {
|
|
r.Use(middleware.Auth(opts.verifier))
|
|
r.Route("/admin", adminHandler.Routes)
|
|
r.Route("/admin/compliance", compHandler.Routes)
|
|
})
|
|
|
|
srv := httptest.NewServer(r)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
// ─── In-memory ComplianceStore for tests ─────────────────────────────────────
|
|
|
|
type memComplianceStore struct {
|
|
mu sync.Mutex
|
|
entries map[string]compliance.ProcessingEntry
|
|
seq int
|
|
}
|
|
|
|
func newMemComplianceStore() *memComplianceStore {
|
|
return &memComplianceStore{entries: make(map[string]compliance.ProcessingEntry)}
|
|
}
|
|
|
|
func (m *memComplianceStore) List(_ context.Context, tenantID string) ([]compliance.ProcessingEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []compliance.ProcessingEntry
|
|
for _, e := range m.entries {
|
|
if e.TenantID == tenantID {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memComplianceStore) Get(_ context.Context, id, tenantID string) (compliance.ProcessingEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.entries[id]
|
|
if !ok || e.TenantID != tenantID {
|
|
return compliance.ProcessingEntry{}, compliance.ErrNotFound
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (m *memComplianceStore) Create(_ context.Context, entry compliance.ProcessingEntry) (compliance.ProcessingEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.seq++
|
|
entry.ID = fmt.Sprintf("comp-%d", m.seq)
|
|
entry.CreatedAt = time.Now()
|
|
entry.UpdatedAt = time.Now()
|
|
m.entries[entry.ID] = entry
|
|
return entry, nil
|
|
}
|
|
|
|
func (m *memComplianceStore) Update(_ context.Context, entry compliance.ProcessingEntry) (compliance.ProcessingEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
existing, ok := m.entries[entry.ID]
|
|
if !ok || existing.TenantID != entry.TenantID {
|
|
return compliance.ProcessingEntry{}, compliance.ErrNotFound
|
|
}
|
|
entry.CreatedAt = existing.CreatedAt
|
|
entry.UpdatedAt = time.Now()
|
|
m.entries[entry.ID] = entry
|
|
return entry, nil
|
|
}
|
|
|
|
func (m *memComplianceStore) Delete(_ context.Context, id, tenantID string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.entries[id]
|
|
if !ok || e.TenantID != tenantID {
|
|
return compliance.ErrNotFound
|
|
}
|
|
delete(m.entries, id)
|
|
return nil
|
|
}
|
|
|
|
// ─── Helper: doAdminJSON ──────────────────────────────────────────────────────
|
|
|
|
// doAdminJSON sends an authenticated JSON request to the test server.
|
|
func doAdminJSON(t *testing.T, method, url string, body interface{}) *http.Response {
|
|
t.Helper()
|
|
return doJSON(t, http.DefaultClient, method, url, "test-token", body)
|
|
}
|
|
|
|
// decodeBody decodes JSON body into dst, draining the response body.
|
|
func decodeBody(t *testing.T, resp *http.Response, dst interface{}) {
|
|
t.Helper()
|
|
defer resp.Body.Close()
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(dst))
|
|
}
|
|
|
|
// ─── E2E tests: Admin Policies ────────────────────────────────────────────────
|
|
|
|
// TestE2E_Admin_Policies_List_Empty verifies GET /v1/admin/policies returns
|
|
// 200 with an empty data array when no policies have been created.
|
|
func TestE2E_Admin_Policies_List_Empty(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
|
|
resp := doAdminJSON(t, http.MethodGet, srv.URL+"/v1/admin/policies", nil)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Data []interface{} `json:"data"`
|
|
}
|
|
decodeBody(t, resp, &body)
|
|
assert.Empty(t, body.Data)
|
|
}
|
|
|
|
// TestE2E_Admin_Policies_Create_Returns201 verifies POST /v1/admin/policies
|
|
// creates a policy and returns 201 with a non-empty ID.
|
|
func TestE2E_Admin_Policies_Create_Returns201(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
|
|
payload := map[string]interface{}{
|
|
"name": "HR policy",
|
|
"priority": 10,
|
|
"is_enabled": true,
|
|
"conditions": []map[string]interface{}{},
|
|
"action": map[string]string{"provider": "openai", "model": "gpt-4o"},
|
|
}
|
|
|
|
resp := doAdminJSON(t, http.MethodPost, srv.URL+"/v1/admin/policies", payload)
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var created routing.RoutingRule
|
|
decodeBody(t, resp, &created)
|
|
assert.NotEmpty(t, created.ID)
|
|
assert.Equal(t, "HR policy", created.Name)
|
|
assert.Equal(t, e2eTenantID, created.TenantID)
|
|
}
|
|
|
|
// TestE2E_Admin_Policies_Update_Returns200 creates a policy then updates it
|
|
// and verifies the returned rule contains the new name.
|
|
func TestE2E_Admin_Policies_Update_Returns200(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
url := srv.URL + "/v1/admin/policies"
|
|
|
|
// Create
|
|
payload := map[string]interface{}{
|
|
"name": "initial",
|
|
"is_enabled": true,
|
|
"conditions": []map[string]interface{}{},
|
|
"action": map[string]string{"provider": "openai", "model": "gpt-4o"},
|
|
}
|
|
createResp := doAdminJSON(t, http.MethodPost, url, payload)
|
|
require.Equal(t, http.StatusCreated, createResp.StatusCode)
|
|
var created routing.RoutingRule
|
|
decodeBody(t, createResp, &created)
|
|
|
|
// Update
|
|
payload["name"] = "updated"
|
|
updateResp := doAdminJSON(t, http.MethodPut, url+"/"+created.ID, payload)
|
|
assert.Equal(t, http.StatusOK, updateResp.StatusCode)
|
|
|
|
var updated routing.RoutingRule
|
|
decodeBody(t, updateResp, &updated)
|
|
assert.Equal(t, "updated", updated.Name)
|
|
assert.Equal(t, created.ID, updated.ID)
|
|
}
|
|
|
|
// TestE2E_Admin_Policies_Delete_Returns204 creates a policy then deletes it,
|
|
// verifying 204 No Content on delete and 404 on subsequent GET.
|
|
func TestE2E_Admin_Policies_Delete_Returns204(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
url := srv.URL + "/v1/admin/policies"
|
|
|
|
// Create
|
|
payload := map[string]interface{}{
|
|
"name": "to-delete",
|
|
"is_enabled": true,
|
|
"conditions": []map[string]interface{}{},
|
|
"action": map[string]string{"provider": "openai", "model": "gpt-4o"},
|
|
}
|
|
createResp := doAdminJSON(t, http.MethodPost, url, payload)
|
|
require.Equal(t, http.StatusCreated, createResp.StatusCode)
|
|
var created routing.RoutingRule
|
|
decodeBody(t, createResp, &created)
|
|
|
|
// Delete
|
|
delResp := doAdminJSON(t, http.MethodDelete, url+"/"+created.ID, nil)
|
|
assert.Equal(t, http.StatusNoContent, delResp.StatusCode)
|
|
delResp.Body.Close()
|
|
|
|
// GET should be 404
|
|
getResp := doAdminJSON(t, http.MethodGet, url+"/"+created.ID, nil)
|
|
assert.Equal(t, http.StatusNotFound, getResp.StatusCode)
|
|
getResp.Body.Close()
|
|
}
|
|
|
|
// TestE2E_Admin_Policies_SeedTemplate_HR verifies that POST
|
|
// /v1/admin/policies/seed/hr returns 201 and creates a rule in the store.
|
|
func TestE2E_Admin_Policies_SeedTemplate_HR(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
|
|
resp := doAdminJSON(t, http.MethodPost, srv.URL+"/v1/admin/policies/seed/hr", nil)
|
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
|
|
|
var created routing.RoutingRule
|
|
decodeBody(t, resp, &created)
|
|
assert.NotEmpty(t, created.ID)
|
|
assert.NotEmpty(t, created.Name)
|
|
assert.Equal(t, e2eTenantID, created.TenantID)
|
|
}
|
|
|
|
// ─── E2E tests: Feature Flags Admin API ──────────────────────────────────────
|
|
|
|
// TestE2E_Admin_Flags_List_Set_Delete exercises the full flag lifecycle:
|
|
// GET (empty), PUT to set, GET (present), DELETE, GET (gone).
|
|
func TestE2E_Admin_Flags_List_Set_Delete(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
flagsURL := srv.URL + "/v1/admin/flags"
|
|
|
|
// GET initial state — may contain global defaults; check 200 at minimum.
|
|
listResp := doAdminJSON(t, http.MethodGet, flagsURL, nil)
|
|
assert.Equal(t, http.StatusOK, listResp.StatusCode)
|
|
listResp.Body.Close()
|
|
|
|
// PUT — disable pii_enabled for this tenant
|
|
putResp := doAdminJSON(t, http.MethodPut, flagsURL+"/pii_enabled",
|
|
map[string]bool{"enabled": false})
|
|
assert.Equal(t, http.StatusOK, putResp.StatusCode)
|
|
|
|
var flag flags.FeatureFlag
|
|
decodeBody(t, putResp, &flag)
|
|
assert.Equal(t, "pii_enabled", flag.Name)
|
|
assert.False(t, flag.IsEnabled)
|
|
|
|
// GET list — should now include pii_enabled for this tenant
|
|
listResp2 := doAdminJSON(t, http.MethodGet, flagsURL, nil)
|
|
assert.Equal(t, http.StatusOK, listResp2.StatusCode)
|
|
var listBody struct {
|
|
Data []flags.FeatureFlag `json:"data"`
|
|
}
|
|
decodeBody(t, listResp2, &listBody)
|
|
found := false
|
|
for _, f := range listBody.Data {
|
|
if f.Name == "pii_enabled" && f.TenantID == e2eTenantID {
|
|
found = true
|
|
assert.False(t, f.IsEnabled)
|
|
}
|
|
}
|
|
assert.True(t, found, "pii_enabled should be in the flag list after PUT")
|
|
|
|
// DELETE
|
|
delResp := doAdminJSON(t, http.MethodDelete, flagsURL+"/pii_enabled", nil)
|
|
assert.Equal(t, http.StatusNoContent, delResp.StatusCode)
|
|
delResp.Body.Close()
|
|
|
|
// After DELETE, the tenant-specific flag should be gone.
|
|
// A subsequent GET /flags should not include the tenant-specific pii_enabled.
|
|
listResp3 := doAdminJSON(t, http.MethodGet, flagsURL, nil)
|
|
assert.Equal(t, http.StatusOK, listResp3.StatusCode)
|
|
var listBody3 struct {
|
|
Data []flags.FeatureFlag `json:"data"`
|
|
}
|
|
decodeBody(t, listResp3, &listBody3)
|
|
for _, f := range listBody3.Data {
|
|
if f.Name == "pii_enabled" {
|
|
assert.NotEqual(t, e2eTenantID, f.TenantID,
|
|
"tenant-specific pii_enabled should be deleted")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── E2E tests: Audit Logs ────────────────────────────────────────────────────
|
|
|
|
// TestE2E_Admin_Logs_Query_ReturnsEntries pre-seeds a MemLogger with one
|
|
// entry and verifies GET /v1/admin/logs returns it.
|
|
func TestE2E_Admin_Logs_Query_ReturnsEntries(t *testing.T) {
|
|
memLog := auditlog.NewMemLogger()
|
|
memLog.Log(auditlog.AuditEntry{
|
|
RequestID: "req-e2e-01",
|
|
TenantID: e2eTenantID,
|
|
UserID: e2eUserID,
|
|
Timestamp: time.Now(),
|
|
ModelRequested: "gpt-4o",
|
|
Provider: "openai",
|
|
TokenTotal: 42,
|
|
Status: "ok",
|
|
})
|
|
|
|
srv := buildAdminServer(t, adminServerOptions{auditLog: memLog})
|
|
|
|
resp := doAdminJSON(t, http.MethodGet, srv.URL+"/v1/admin/logs", nil)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var result auditlog.AuditResult
|
|
decodeBody(t, resp, &result)
|
|
require.Equal(t, 1, result.Total, "should have exactly one log entry")
|
|
assert.Equal(t, "req-e2e-01", result.Data[0].RequestID)
|
|
}
|
|
|
|
// ─── E2E tests: Costs ─────────────────────────────────────────────────────────
|
|
|
|
// TestE2E_Admin_Costs_GroupBy_Provider verifies GET /v1/admin/costs?group_by=provider
|
|
// returns a data array that includes an openai cost summary.
|
|
func TestE2E_Admin_Costs_GroupBy_Provider(t *testing.T) {
|
|
memLog := auditlog.NewMemLogger()
|
|
// Seed two entries with different providers
|
|
for i := 0; i < 3; i++ {
|
|
memLog.Log(auditlog.AuditEntry{
|
|
RequestID: fmt.Sprintf("req-%d", i),
|
|
TenantID: e2eTenantID,
|
|
Provider: "openai",
|
|
TokenTotal: 100,
|
|
CostUSD: 0.002,
|
|
Status: "ok",
|
|
})
|
|
}
|
|
memLog.Log(auditlog.AuditEntry{
|
|
RequestID: "req-mistral",
|
|
TenantID: e2eTenantID,
|
|
Provider: "mistral",
|
|
TokenTotal: 50,
|
|
CostUSD: 0.001,
|
|
Status: "ok",
|
|
})
|
|
|
|
srv := buildAdminServer(t, adminServerOptions{auditLog: memLog})
|
|
|
|
resp := doAdminJSON(t, http.MethodGet, srv.URL+"/v1/admin/costs?group_by=provider", nil)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var result auditlog.CostResult
|
|
decodeBody(t, resp, &result)
|
|
require.NotEmpty(t, result.Data)
|
|
|
|
keySet := make(map[string]bool)
|
|
for _, s := range result.Data {
|
|
keySet[s.Key] = true
|
|
}
|
|
assert.True(t, keySet["openai"], "openai should appear in cost breakdown")
|
|
assert.True(t, keySet["mistral"], "mistral should appear in cost breakdown")
|
|
}
|
|
|
|
// ─── E2E tests: Compliance Entry CRUD ────────────────────────────────────────
|
|
|
|
// TestE2E_Compliance_Entry_CRUD exercises the full processing entry lifecycle:
|
|
// POST → GET → PUT → DELETE.
|
|
func TestE2E_Compliance_Entry_CRUD(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
baseURL := srv.URL + "/v1/admin/compliance/entries"
|
|
|
|
// POST — create entry
|
|
createPayload := map[string]interface{}{
|
|
"use_case_name": "Chatbot RH",
|
|
"legal_basis": "legitimate_interest",
|
|
"purpose": "Automatisation des réponses RH internes",
|
|
"data_categories": []string{"identifiers", "professional"},
|
|
"recipients": []string{"HR team"},
|
|
"processors": []string{"OpenAI Inc."},
|
|
"retention_period": "12 months",
|
|
"security_measures": "AES-256 encryption, access control",
|
|
"controller_name": "Veylant E2E Corp",
|
|
}
|
|
createResp := doAdminJSON(t, http.MethodPost, baseURL, createPayload)
|
|
require.Equal(t, http.StatusCreated, createResp.StatusCode)
|
|
|
|
var created compliance.ProcessingEntry
|
|
decodeBody(t, createResp, &created)
|
|
require.NotEmpty(t, created.ID)
|
|
assert.Equal(t, "Chatbot RH", created.UseCaseName)
|
|
assert.Equal(t, e2eTenantID, created.TenantID)
|
|
|
|
// GET — retrieve entry
|
|
getResp := doAdminJSON(t, http.MethodGet, baseURL+"/"+created.ID, nil)
|
|
assert.Equal(t, http.StatusOK, getResp.StatusCode)
|
|
var fetched compliance.ProcessingEntry
|
|
decodeBody(t, getResp, &fetched)
|
|
assert.Equal(t, created.ID, fetched.ID)
|
|
|
|
// PUT — update entry
|
|
createPayload["use_case_name"] = "Chatbot RH v2"
|
|
updateResp := doAdminJSON(t, http.MethodPut, baseURL+"/"+created.ID, createPayload)
|
|
assert.Equal(t, http.StatusOK, updateResp.StatusCode)
|
|
var updated compliance.ProcessingEntry
|
|
decodeBody(t, updateResp, &updated)
|
|
assert.Equal(t, "Chatbot RH v2", updated.UseCaseName)
|
|
|
|
// DELETE — remove entry
|
|
delResp := doAdminJSON(t, http.MethodDelete, baseURL+"/"+created.ID, nil)
|
|
assert.Equal(t, http.StatusNoContent, delResp.StatusCode)
|
|
delResp.Body.Close()
|
|
|
|
// GET after delete — should be 404
|
|
get2Resp := doAdminJSON(t, http.MethodGet, baseURL+"/"+created.ID, nil)
|
|
assert.Equal(t, http.StatusNotFound, get2Resp.StatusCode)
|
|
get2Resp.Body.Close()
|
|
}
|
|
|
|
// ─── E2E tests: GDPR Erasure ─────────────────────────────────────────────────
|
|
|
|
// TestE2E_Compliance_GDPR_Erase_Returns200 verifies DELETE
|
|
// /v1/admin/compliance/gdpr/erase/{user_id} returns 200 with status "completed".
|
|
// The handler works without a DB connection (db=nil path is exercised).
|
|
func TestE2E_Compliance_GDPR_Erase_Returns200(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
targetUser := "user-to-erase@corp.example"
|
|
|
|
resp, err := makeRequest(t,
|
|
http.MethodDelete,
|
|
srv.URL+"/v1/admin/compliance/gdpr/erase/"+targetUser+"?reason=test-erase",
|
|
"test-token",
|
|
nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var erasure compliance.ErasureRecord
|
|
decodeBody(t, resp, &erasure)
|
|
assert.Equal(t, "completed", erasure.Status)
|
|
assert.Equal(t, targetUser, erasure.TargetUser)
|
|
assert.Equal(t, e2eTenantID, erasure.TenantID)
|
|
}
|
|
|
|
// makeRequest is a low-level helper for requests that don't have a JSON body.
|
|
func makeRequest(t *testing.T, method, url, token string, body []byte) (*http.Response, error) {
|
|
t.Helper()
|
|
var bodyReader *bytes.Reader
|
|
if body != nil {
|
|
bodyReader = bytes.NewReader(body)
|
|
} else {
|
|
bodyReader = bytes.NewReader(nil)
|
|
}
|
|
req, err := http.NewRequestWithContext(context.Background(), method, url, bodyReader)
|
|
require.NoError(t, err)
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
return http.DefaultClient.Do(req)
|
|
}
|
|
|
|
// ─── E2E tests: Admin server — no-auth guard ──────────────────────────────────
|
|
|
|
// TestE2E_Admin_NoToken_Returns401 verifies that all admin routes are
|
|
// protected: a request without a token receives 401.
|
|
func TestE2E_Admin_NoToken_Returns401(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
|
|
req, err := http.NewRequestWithContext(context.Background(),
|
|
http.MethodGet, srv.URL+"/v1/admin/policies", nil)
|
|
require.NoError(t, err)
|
|
// No Authorization header
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
// ─── E2E tests: Routing flag via proxy ────────────────────────────────────────
|
|
|
|
// TestE2E_Admin_Flags_Routing_Disabled_StillRoutes verifies that when
|
|
// routing_enabled is set to false for a tenant, the proxy falls back to
|
|
// static prefix rules and still returns 200 (not an error).
|
|
func TestE2E_Admin_Flags_Routing_Disabled_StillRoutes(t *testing.T) {
|
|
fs := flags.NewMemFlagStore()
|
|
// Explicitly disable routing for the E2E tenant
|
|
_, err := fs.Set(context.Background(), e2eTenantID, "routing_enabled", false)
|
|
require.NoError(t, err)
|
|
|
|
// Build proxy server using the flag store with routing disabled
|
|
srv := buildProxyServer(t, proxyServerOptions{flagStore: fs})
|
|
|
|
resp := doJSON(t, http.DefaultClient, http.MethodPost,
|
|
srv.URL+"/v1/chat/completions",
|
|
"test-token",
|
|
chatBody("gpt-4o", false),
|
|
)
|
|
defer resp.Body.Close()
|
|
|
|
// Should still succeed — static prefix fallback routes gpt-4o → openai stub
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body map[string]interface{}
|
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
|
|
choices, _ := body["choices"].([]interface{})
|
|
assert.NotEmpty(t, choices)
|
|
}
|
|
|
|
// ─── E2E tests: Compliance list empty ────────────────────────────────────────
|
|
|
|
// TestE2E_Compliance_Entries_List_Empty verifies that GET /v1/admin/compliance/entries
|
|
// returns 200 with an empty data array on a fresh store.
|
|
func TestE2E_Compliance_Entries_List_Empty(t *testing.T) {
|
|
srv := buildAdminServer(t, adminServerOptions{})
|
|
|
|
resp := doAdminJSON(t, http.MethodGet, srv.URL+"/v1/admin/compliance/entries", nil)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var body struct {
|
|
Data []interface{} `json:"data"`
|
|
}
|
|
decodeBody(t, resp, &body)
|
|
assert.Empty(t, body.Data)
|
|
}
|
|
|