Changeset 0.10.0 (#56)

This commit is contained in:
2026-02-24 14:50:53 +00:00
parent cdfd69bad3
commit ea03f956ca
31 changed files with 3303 additions and 167 deletions

View File

@@ -16,6 +16,7 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/config"
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/middleware"
"git.gobha.me/xcaliber/chat-switchboard/roles"
"git.gobha.me/xcaliber/chat-switchboard/store/postgres"
)
@@ -42,6 +43,9 @@ func setupHarness(t *testing.T) *testHarness {
stores := postgres.NewStores(database.TestDB)
// Roles resolver (nil vault — test-fire won't work, but CRUD will)
roleResolver := roles.NewResolver(stores, nil)
r := gin.New()
api := r.Group("/api/v1")
@@ -52,7 +56,7 @@ func setupHarness(t *testing.T) *testHarness {
authGroup.POST("/login", auth.Login)
// Public settings
adm := NewAdminHandler(stores, nil)
adm := NewAdminHandler(stores, nil, nil)
api.GET("/settings/public", adm.PublicSettings)
// Protected routes
@@ -91,10 +95,24 @@ func setupHarness(t *testing.T) *testHarness {
teamScoped.PUT("/providers/:id", teams.UpdateTeamProvider)
teamScoped.DELETE("/providers/:id", teams.DeleteTeamProvider)
teamScoped.GET("/providers/:id/models", teams.ListTeamProviderModels)
// Team usage
teamUsage := NewUsageHandler(stores)
teamScoped.GET("/usage", teamUsage.TeamUsage)
// Team roles
teamRoles := NewRolesHandler(stores, roleResolver)
teamScoped.GET("/roles", teamRoles.ListTeamRoles)
teamScoped.PUT("/roles/:role", teamRoles.UpdateTeamRole)
teamScoped.DELETE("/roles/:role", teamRoles.DeleteTeamRole)
}
// User usage
usage := NewUsageHandler(stores)
protected.GET("/usage", usage.PersonalUsage)
// Profile / Settings
settings := NewSettingsHandler()
settings := NewSettingsHandler(nil)
protected.GET("/profile", settings.GetProfile)
// Presets
@@ -116,7 +134,7 @@ func setupHarness(t *testing.T) *testHarness {
protected.POST("/channels", channels.CreateChannel)
// Completions
completions := NewCompletionHandler(nil)
completions := NewCompletionHandler(nil, stores)
protected.POST("/chat/completions", completions.Complete)
// Admin routes
@@ -143,6 +161,21 @@ func setupHarness(t *testing.T) *testHarness {
admin.GET("/presets", presets.ListAdminPersonas)
admin.POST("/presets", presets.CreateAdminPersona)
// Admin roles
rolesH := NewRolesHandler(stores, roleResolver)
admin.GET("/roles", rolesH.ListRoles)
admin.GET("/roles/:role", rolesH.GetRole)
admin.PUT("/roles/:role", rolesH.UpdateRole)
// Admin usage + pricing
usageH := NewUsageHandler(stores)
admin.GET("/usage", usageH.AdminUsage)
admin.GET("/usage/users/:id", usageH.AdminUserUsage)
admin.GET("/usage/teams/:id", usageH.AdminTeamUsage)
admin.GET("/pricing", usageH.ListPricing)
admin.PUT("/pricing", usageH.UpsertPricing)
admin.DELETE("/pricing/:provider/:model", usageH.DeletePricing)
return &testHarness{router: r, t: t}
}
@@ -1561,3 +1594,551 @@ func TestUserJourney_FullMatrix(t *testing.T) {
}
})
}
// ═══════════════════════════════════════════
// ROLES TESTS
// ═══════════════════════════════════════════
func TestIntegration_Roles_ListReturnsSeeded(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
w := h.request("GET", "/api/v1/admin/roles", adminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("list roles: want 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
decode(w, &resp)
// Migration 004 seeds utility, embedding, generation
for _, role := range []string{"utility", "embedding", "generation"} {
if _, ok := resp[role]; !ok {
t.Errorf("expected role %q in response, got: %v", role, resp)
}
}
}
func TestIntegration_Roles_UpdateAndGet(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
// Create a global provider to reference
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "TestProvider", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-test",
})
if w.Code != http.StatusCreated {
t.Fatalf("create config: %d: %s", w.Code, w.Body.String())
}
var cfg map[string]interface{}
decode(w, &cfg)
cfgID := cfg["id"].(string)
// Update utility role
w = h.request("PUT", "/api/v1/admin/roles/utility", adminToken, map[string]interface{}{
"primary": map[string]string{
"provider_config_id": cfgID,
"model_id": "gpt-4o-mini",
},
})
if w.Code != http.StatusOK {
t.Fatalf("update role: want 200, got %d: %s", w.Code, w.Body.String())
}
// Verify via list
w = h.request("GET", "/api/v1/admin/roles", adminToken, nil)
var allRoles map[string]interface{}
decode(w, &allRoles)
utilRaw, ok := allRoles["utility"]
if !ok {
t.Fatal("utility role missing after update")
}
utilMap, ok := utilRaw.(map[string]interface{})
if !ok {
t.Fatalf("utility role unexpected type: %T", utilRaw)
}
primary, _ := utilMap["primary"].(map[string]interface{})
if primary == nil {
t.Fatal("utility primary should be set")
}
if primary["model_id"] != "gpt-4o-mini" {
t.Errorf("utility primary model: want gpt-4o-mini, got %v", primary["model_id"])
}
}
func TestIntegration_Roles_InvalidRole400(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
w := h.request("GET", "/api/v1/admin/roles/nonexistent", adminToken, nil)
if w.Code != http.StatusBadRequest {
t.Fatalf("invalid role: want 400, got %d", w.Code)
}
}
func TestIntegration_Roles_NonAdmin403(t *testing.T) {
h := setupHarness(t)
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
_, userToken := h.registerUser("user1", "user1@test.com", "password123")
w := h.request("GET", "/api/v1/admin/roles", userToken, nil)
if w.Code != http.StatusForbidden {
t.Fatalf("non-admin roles: want 403, got %d", w.Code)
}
}
// ── Team Roles ────────────────────────────
func TestIntegration_TeamRoles_CRUD(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
// Create team admin user
teamAdminID := database.SeedTestUser(t, "teamlead", "teamlead@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", teamAdminID)
teamAdminToken := makeToken(teamAdminID, "teamlead@test.com", "user")
// Create team
w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{
"name": "Eng", "description": "Engineering",
})
var team map[string]interface{}
decode(w, &team)
teamID := team["id"].(string)
// Add team admin
h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken,
map[string]string{"user_id": teamAdminID, "role": "admin"})
// Create a provider to reference
w = h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "Provider1", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-test",
})
var pcfg map[string]interface{}
decode(w, &pcfg)
cfgID := pcfg["id"].(string)
// List team roles — initially empty
w = h.request("GET", fmt.Sprintf("/api/v1/teams/%s/roles", teamID), teamAdminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("list team roles: %d: %s", w.Code, w.Body.String())
}
var roleList map[string]interface{}
decode(w, &roleList)
if len(roleList) != 0 {
t.Fatalf("team roles should be empty initially, got %d", len(roleList))
}
// Set team role override
w = h.request("PUT", fmt.Sprintf("/api/v1/teams/%s/roles/utility", teamID), teamAdminToken,
map[string]interface{}{
"primary": map[string]string{
"provider_config_id": cfgID,
"model_id": "gpt-4o",
},
})
if w.Code != http.StatusOK {
t.Fatalf("set team role: %d: %s", w.Code, w.Body.String())
}
// List should now have override
w = h.request("GET", fmt.Sprintf("/api/v1/teams/%s/roles", teamID), teamAdminToken, nil)
decode(w, &roleList)
if _, ok := roleList["utility"]; !ok {
t.Fatal("team roles should have utility after update")
}
// Delete override
w = h.request("DELETE", fmt.Sprintf("/api/v1/teams/%s/roles/utility", teamID), teamAdminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("delete team role: %d: %s", w.Code, w.Body.String())
}
// List should be empty again
w = h.request("GET", fmt.Sprintf("/api/v1/teams/%s/roles", teamID), teamAdminToken, nil)
roleList = map[string]interface{}{} // reset — json.Unmarshal merges into existing maps
decode(w, &roleList)
if _, ok := roleList["utility"]; ok {
t.Fatal("team roles should not have utility after delete")
}
}
// ═══════════════════════════════════════════
// USAGE TESTS
// ═══════════════════════════════════════════
// seedUsage inserts a usage_log row directly for testing.
func seedUsage(t *testing.T, userID, provCfgID, model, scope string, prompt, completion int, costIn, costOut float64) {
t.Helper()
_, err := database.TestDB.Exec(`
INSERT INTO usage_log (user_id, provider_config_id, provider_scope,
model_id, prompt_tokens, completion_tokens,
cost_input, cost_output)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, userID, provCfgID, scope, model, prompt, completion, costIn, costOut)
if err != nil {
t.Fatalf("seedUsage: %v", err)
}
}
func TestIntegration_Usage_AdminView(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
userID := database.SeedTestUser(t, "alice", "alice@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", userID)
// Create global provider
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "TestOAI", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-test",
})
var cfg map[string]interface{}
decode(w, &cfg)
cfgID := cfg["id"].(string)
// Seed usage data
seedUsage(t, userID, cfgID, "gpt-4o", "global", 1000, 200, 0.005, 0.006)
seedUsage(t, userID, cfgID, "gpt-4o", "global", 500, 100, 0.0025, 0.003)
seedUsage(t, userID, cfgID, "gpt-4o-mini", "global", 2000, 400, 0.001, 0.002)
// Admin usage — grouped by model (default)
w = h.request("GET", "/api/v1/admin/usage?group_by=model", adminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("admin usage: %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
decode(w, &resp)
totals := resp["totals"].(map[string]interface{})
if int(totals["requests"].(float64)) != 3 {
t.Errorf("totals.requests: want 3, got %v", totals["requests"])
}
if int(totals["input_tokens"].(float64)) != 3500 {
t.Errorf("totals.input_tokens: want 3500, got %v", totals["input_tokens"])
}
results := resp["results"].([]interface{})
if len(results) != 2 {
t.Fatalf("want 2 model groups, got %d", len(results))
}
}
func TestIntegration_Usage_AdminExcludesBYOK(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
userID := database.SeedTestUser(t, "bob", "bob@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", userID)
// Create global provider
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "Global1", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-g",
})
var cfg map[string]interface{}
decode(w, &cfg)
globalCfgID := cfg["id"].(string)
// Seed: 1 global usage + 1 personal BYOK usage
seedUsage(t, userID, globalCfgID, "gpt-4o", "global", 1000, 200, 0.01, 0.01)
seedUsage(t, userID, globalCfgID, "gpt-4o", "personal", 500, 100, 0.005, 0.005)
// Admin view should exclude personal
w = h.request("GET", "/api/v1/admin/usage", adminToken, nil)
var resp map[string]interface{}
decode(w, &resp)
totals := resp["totals"].(map[string]interface{})
if int(totals["requests"].(float64)) != 1 {
t.Errorf("admin should see 1 request (excludes BYOK), got %v", totals["requests"])
}
}
func TestIntegration_Usage_PersonalIncludesBYOK(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
userID, userToken := h.registerUser("carol", "carol@test.com", "password123")
// Create global provider
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "Global2", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-g2",
})
var cfg map[string]interface{}
decode(w, &cfg)
cfgID := cfg["id"].(string)
seedUsage(t, userID, cfgID, "gpt-4o", "global", 1000, 200, 0.01, 0.01)
seedUsage(t, userID, cfgID, "gpt-4o", "personal", 500, 100, 0.005, 0.005)
// Personal view should include all scopes
w = h.request("GET", "/api/v1/usage", userToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("personal usage: %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
decode(w, &resp)
totals := resp["totals"].(map[string]interface{})
if int(totals["requests"].(float64)) != 2 {
t.Errorf("personal should see 2 requests (includes BYOK), got %v", totals["requests"])
}
}
func TestIntegration_Usage_NonAdmin403(t *testing.T) {
h := setupHarness(t)
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
_, userToken := h.registerUser("dave", "dave@test.com", "password123")
w := h.request("GET", "/api/v1/admin/usage", userToken, nil)
if w.Code != http.StatusForbidden {
t.Fatalf("non-admin usage: want 403, got %d", w.Code)
}
}
// ── Team Usage ────────────────────────────
func TestIntegration_Usage_TeamAdmin(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
// Create team admin + member
teamAdminID := database.SeedTestUser(t, "teamlead2", "teamlead2@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", teamAdminID)
teamAdminToken := makeToken(teamAdminID, "teamlead2@test.com", "user")
memberID := database.SeedTestUser(t, "member2", "member2@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", memberID)
// Create team
w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{
"name": "EngTeam", "description": "Test",
})
var team map[string]interface{}
decode(w, &team)
teamID := team["id"].(string)
h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken,
map[string]string{"user_id": teamAdminID, "role": "admin"})
h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken,
map[string]string{"user_id": memberID, "role": "member"})
// Create team provider via team admin self-service
w = h.request("POST", fmt.Sprintf("/api/v1/teams/%s/providers", teamID), teamAdminToken,
map[string]interface{}{
"name": "TeamOpenAI", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-team",
})
if w.Code != http.StatusCreated {
t.Fatalf("create team provider: %d: %s", w.Code, w.Body.String())
}
var tprov map[string]interface{}
decode(w, &tprov)
teamProvID := tprov["id"].(string)
// Also create a global provider (usage against it should NOT appear in team view)
w = h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "GlobalProv", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-global",
})
var gcfg map[string]interface{}
decode(w, &gcfg)
globalProvID := gcfg["id"].(string)
// Seed: 2 rows against team provider, 1 against global
seedUsage(t, memberID, teamProvID, "gpt-4o", "team", 1000, 200, 0.01, 0.01)
seedUsage(t, teamAdminID, teamProvID, "gpt-4o", "team", 500, 100, 0.005, 0.005)
seedUsage(t, memberID, globalProvID, "gpt-4o", "global", 2000, 400, 0.02, 0.02)
// Team usage should show only team provider usage (2 rows)
w = h.request("GET", fmt.Sprintf("/api/v1/teams/%s/usage", teamID), teamAdminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("team usage: %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
decode(w, &resp)
totals := resp["totals"].(map[string]interface{})
if int(totals["requests"].(float64)) != 2 {
t.Errorf("team usage should show 2 requests (team provider only), got %v", totals["requests"])
}
if int(totals["input_tokens"].(float64)) != 1500 {
t.Errorf("team usage input_tokens: want 1500, got %v", totals["input_tokens"])
}
}
func TestIntegration_Usage_TeamNonAdmin403(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
memberID := database.SeedTestUser(t, "member3", "member3@test.com")
database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", memberID)
memberToken := makeToken(memberID, "member3@test.com", "user")
// Create team, add member (NOT admin)
w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{
"name": "EngTeam2", "description": "Test2",
})
var team map[string]interface{}
decode(w, &team)
teamID := team["id"].(string)
h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken,
map[string]string{"user_id": memberID, "role": "member"})
// Non-admin member should be forbidden
w = h.request("GET", fmt.Sprintf("/api/v1/teams/%s/usage", teamID), memberToken, nil)
if w.Code != http.StatusForbidden {
t.Fatalf("team usage non-admin: want 403, got %d", w.Code)
}
}
// ═══════════════════════════════════════════
// PRICING TESTS
// ═══════════════════════════════════════════
func TestIntegration_Pricing_CRUD(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
// Create provider to reference
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "PricingProv", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-p",
})
var cfg map[string]interface{}
decode(w, &cfg)
cfgID := cfg["id"].(string)
// Upsert pricing
w = h.request("PUT", "/api/v1/admin/pricing", adminToken, map[string]interface{}{
"provider_config_id": cfgID,
"model_id": "gpt-4o",
"input_per_m": 2.5,
"output_per_m": 10.0,
})
if w.Code != http.StatusOK {
t.Fatalf("upsert pricing: %d: %s", w.Code, w.Body.String())
}
// List pricing
w = h.request("GET", "/api/v1/admin/pricing", adminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("list pricing: %d: %s", w.Code, w.Body.String())
}
var entries []interface{}
decode(w, &entries)
if len(entries) != 1 {
t.Fatalf("want 1 pricing entry, got %d", len(entries))
}
entry := entries[0].(map[string]interface{})
if entry["model_id"] != "gpt-4o" {
t.Errorf("pricing model: want gpt-4o, got %v", entry["model_id"])
}
if entry["source"] != "manual" {
t.Errorf("pricing source: want manual, got %v", entry["source"])
}
// Delete pricing
w = h.request("DELETE", fmt.Sprintf("/api/v1/admin/pricing/%s/gpt-4o", cfgID), adminToken, nil)
if w.Code != http.StatusOK {
t.Fatalf("delete pricing: %d: %s", w.Code, w.Body.String())
}
// List should be empty
w = h.request("GET", "/api/v1/admin/pricing", adminToken, nil)
decode(w, &entries)
if len(entries) != 0 {
t.Fatalf("want 0 pricing entries after delete, got %d", len(entries))
}
}
func TestIntegration_Pricing_ExcludesBYOK(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_byok', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
_, userToken := h.registerUser("byokuser", "byokuser@test.com", "password123")
// Global provider with pricing
w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{
"name": "GlobalP", "provider": "openai",
"endpoint": "https://api.openai.com/v1", "api_key": "sk-global",
})
var gcfg map[string]interface{}
decode(w, &gcfg)
globalID := gcfg["id"].(string)
// Set global pricing
h.request("PUT", "/api/v1/admin/pricing", adminToken, map[string]interface{}{
"provider_config_id": globalID,
"model_id": "gpt-4o",
"input_per_m": 2.5,
"output_per_m": 10.0,
})
// BYOK provider (personal scope)
w = h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{
"name": "MyKey", "provider": "openai",
"endpoint": "http://localhost:1/v1", "api_key": "sk-byok",
})
if w.Code != http.StatusCreated {
t.Fatalf("create BYOK: %d: %s", w.Code, w.Body.String())
}
var bcfg map[string]interface{}
decode(w, &bcfg)
byokID := bcfg["id"].(string)
// Simulate catalog pricing for BYOK provider (as model sync would)
database.TestDB.Exec(`
INSERT INTO model_pricing (provider_config_id, model_id, input_per_m, output_per_m, source)
VALUES ($1, 'gpt-4o-byok', 3.0, 15.0, 'catalog')
`, byokID)
// Admin list should only show the 1 global entry, not the BYOK one
w = h.request("GET", "/api/v1/admin/pricing", adminToken, nil)
var entries []interface{}
decode(w, &entries)
if len(entries) != 1 {
t.Fatalf("admin pricing should show 1 (global only), got %d", len(entries))
}
entry := entries[0].(map[string]interface{})
if entry["provider_config_id"] != globalID {
t.Errorf("pricing entry should be global provider, got %v", entry["provider_config_id"])
}
}
func TestIntegration_Pricing_RejectBYOKUpsert(t *testing.T) {
h := setupHarness(t)
_, adminToken := h.createAdminUser("admin", "admin@test.com")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_registration', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('default_user_active', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_byok', 'true') ON CONFLICT (key) DO UPDATE SET value = 'true'")
_, userToken := h.registerUser("byokuser2", "byokuser2@test.com", "password123")
// Create BYOK provider
w := h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{
"name": "MyKey2", "provider": "openai",
"endpoint": "http://localhost:1/v1", "api_key": "sk-byok2",
})
var bcfg map[string]interface{}
decode(w, &bcfg)
byokID := bcfg["id"].(string)
// Admin tries to set pricing on personal provider — should be rejected
w = h.request("PUT", "/api/v1/admin/pricing", adminToken, map[string]interface{}{
"provider_config_id": byokID,
"model_id": "gpt-4o",
"input_per_m": 2.5,
"output_per_m": 10.0,
})
if w.Code != http.StatusForbidden {
t.Fatalf("pricing BYOK upsert: want 403, got %d: %s", w.Code, w.Body.String())
}
}