package handlers import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" "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/store/postgres" ) // ── Test Harness ──────────────────────────── const testJWTSecret = "test-secret-key-for-integration-tests" type testHarness struct { router *gin.Engine t *testing.T } // setupHarness creates a full API router backed by the test database. // Call database.RequireTestDB(t) + database.TruncateAll(t) before using. func setupHarness(t *testing.T) *testHarness { t.Helper() database.RequireTestDB(t) database.TruncateAll(t) cfg := &config.Config{ JWTSecret: testJWTSecret, BasePath: "", } stores := postgres.NewStores(database.TestDB) r := gin.New() api := r.Group("/api/v1") // Auth (unprotected) auth := NewAuthHandler(cfg, stores) authGroup := api.Group("/auth") authGroup.POST("/register", auth.Register) authGroup.POST("/login", auth.Login) // Public settings adm := NewAdminHandler(stores) api.GET("/settings/public", adm.PublicSettings) // Protected routes protected := api.Group("") protected.Use(middleware.Auth(cfg)) // Models models := NewModelHandler(stores) protected.GET("/models/enabled", models.ListEnabledModels) // Model prefs modelPrefs := NewModelPrefsHandler(stores) protected.GET("/models/preferences", modelPrefs.GetPreferences) protected.PUT("/models/preferences", modelPrefs.SetPreference) protected.POST("/models/preferences/bulk", modelPrefs.BulkSetPreferences) // User providers provCfg := NewProviderConfigHandler(stores) protected.GET("/api-configs", provCfg.ListConfigs) protected.POST("/api-configs", provCfg.CreateConfig) protected.GET("/api-configs/:id", provCfg.GetConfig) protected.PUT("/api-configs/:id", provCfg.UpdateConfig) protected.DELETE("/api-configs/:id", provCfg.DeleteConfig) protected.GET("/api-configs/:id/models", provCfg.ListModels) protected.POST("/api-configs/:id/models/fetch", provCfg.FetchModels) // Team self-service (same route group as production) teams := NewTeamHandler() protected.GET("/teams/mine", teams.MyTeams) teamScoped := protected.Group("/teams/:teamId") teamScoped.Use(middleware.RequireTeamAdmin()) { teamScoped.GET("/providers", teams.ListTeamProviders) teamScoped.POST("/providers", teams.CreateTeamProvider) teamScoped.PUT("/providers/:id", teams.UpdateTeamProvider) teamScoped.DELETE("/providers/:id", teams.DeleteTeamProvider) teamScoped.GET("/providers/:id/models", teams.ListTeamProviderModels) } // Profile / Settings settings := NewSettingsHandler() protected.GET("/profile", settings.GetProfile) // Presets presets := NewPersonaHandler(stores) protected.GET("/presets", presets.ListUserPersonas) protected.POST("/presets", presets.CreateUserPersona) // Notes notes := NewNoteHandler() protected.GET("/notes", notes.List) protected.POST("/notes", notes.Create) protected.GET("/notes/:id", notes.Get) protected.PUT("/notes/:id", notes.Update) protected.DELETE("/notes/:id", notes.Delete) // Channels channels := NewChannelHandler() protected.GET("/channels", channels.ListChannels) protected.POST("/channels", channels.CreateChannel) // Completions completions := NewCompletionHandler() protected.POST("/chat/completions", completions.Complete) // Admin routes admin := api.Group("/admin") admin.Use(middleware.Auth(cfg), middleware.RequireAdmin()) admin.GET("/users", adm.ListUsers) admin.POST("/users", adm.CreateUser) admin.PUT("/users/:id/active", adm.ToggleUserActive) admin.GET("/settings", adm.ListGlobalSettings) admin.PUT("/settings/:key", adm.UpdateGlobalSetting) admin.GET("/configs", adm.ListGlobalConfigs) admin.POST("/configs", adm.CreateGlobalConfig) admin.PUT("/configs/:id", adm.UpdateGlobalConfig) admin.DELETE("/configs/:id", adm.DeleteGlobalConfig) admin.GET("/models", adm.ListModelConfigs) admin.PUT("/models/:id", adm.UpdateModelConfig) admin.PUT("/models/bulk", adm.BulkUpdateModels) admin.POST("/models/fetch", adm.FetchModels) admin.GET("/teams", teams.ListTeams) admin.POST("/teams", teams.CreateTeam) admin.GET("/teams/:id", teams.GetTeam) admin.GET("/teams/:id/members", teams.ListMembers) admin.POST("/teams/:id/members", teams.AddMember) admin.GET("/presets", presets.ListAdminPersonas) admin.POST("/presets", presets.CreateAdminPersona) return &testHarness{router: r, t: t} } // makeToken generates a valid JWT for a test user. func makeToken(userID, email, role string) string { claims := Claims{ UserID: userID, Email: email, Role: role, RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), Issuer: "chat-switchboard", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) s, _ := token.SignedString([]byte(testJWTSecret)) return s } // request makes an HTTP request and returns the response recorder. func (h *testHarness) request(method, path, token string, body interface{}) *httptest.ResponseRecorder { h.t.Helper() var reader io.Reader if body != nil { b, _ := json.Marshal(body) reader = bytes.NewReader(b) } req := httptest.NewRequest(method, path, reader) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() h.router.ServeHTTP(w, req) return w } // decode JSON response into target. func decode(w *httptest.ResponseRecorder, target interface{}) error { return json.Unmarshal(w.Body.Bytes(), target) } // ── Seed Helpers ──────────────────────────── // registerUser registers a user via the API and returns the user_id + token. func (h *testHarness) registerUser(username, email, password string) (userID, token string) { h.t.Helper() w := h.request("POST", "/api/v1/auth/register", "", map[string]string{ "username": username, "email": email, "password": password, }) if w.Code != http.StatusCreated && w.Code != http.StatusOK { h.t.Fatalf("register %s: want 200/201, got %d: %s", username, w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) token, _ = resp["access_token"].(string) // Extract user_id from profile w2 := h.request("GET", "/api/v1/profile", token, nil) var profile map[string]interface{} decode(w2, &profile) userID, _ = profile["id"].(string) return userID, token } // createAdminUser seeds an admin directly in DB, returns user_id + token. func (h *testHarness) createAdminUser(username, email string) (userID, token string) { h.t.Helper() userID = database.SeedTestUser(h.t, username, email) // Make admin database.TestDB.Exec("UPDATE users SET role = 'admin', is_active = true WHERE id = $1", userID) token = makeToken(userID, email, "admin") return } // ═══════════════════════════════════════════ // TESTS // ═══════════════════════════════════════════ // ── 1. Auth ───────────────────────────────── func TestIntegration_Auth_Register(t *testing.T) { h := setupHarness(t) // Enable registration policy 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'") w := h.request("POST", "/api/v1/auth/register", "", map[string]string{ "username": "alice", "email": "alice@test.com", "password": "password123", }) if w.Code != http.StatusCreated { t.Fatalf("register: want 201, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) if resp["access_token"] == nil || resp["access_token"] == "" { t.Fatal("register should return access_token") } } func TestIntegration_Auth_Login(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'") h.registerUser("bob", "bob@test.com", "password123") w := h.request("POST", "/api/v1/auth/login", "", map[string]string{ "login": "bob@test.com", "password": "password123", }) if w.Code != http.StatusOK { t.Fatalf("login: want 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) if resp["access_token"] == nil { t.Fatal("login should return access_token") } } func TestIntegration_Auth_ProfileRequiresToken(t *testing.T) { h := setupHarness(t) w := h.request("GET", "/api/v1/profile", "", nil) if w.Code != http.StatusUnauthorized { t.Fatalf("profile without token: want 401, got %d", w.Code) } } // ── 2. Admin Users ────────────────────────── func TestIntegration_AdminListUsers(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // page/per_page support w := h.request("GET", "/api/v1/admin/users?page=1&per_page=50", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("admin list users: want 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) users, ok := resp["users"].([]interface{}) if !ok { t.Fatalf("response must have 'users' array, got %T", resp["users"]) } if len(users) < 1 { t.Fatal("should have at least 1 user (admin)") } total, ok := resp["total"].(float64) if !ok || total < 1 { t.Fatalf("response must have 'total' >= 1, got %v", resp["total"]) } } func TestIntegration_AdminListUsers_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/users?page=1&per_page=50", userToken, nil) if w.Code != http.StatusForbidden { t.Fatalf("non-admin list users: want 403, got %d", w.Code) } } // ── 3. Admin Settings / Policies ──────────── func TestIntegration_AdminPolicyRoundtrip(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") policies := []struct { key string value string }{ {"allow_registration", "false"}, {"allow_user_byok", "true"}, {"allow_user_personas", "true"}, {"default_user_active", "true"}, } for _, p := range policies { w := h.request("PUT", "/api/v1/admin/settings/"+p.key, adminToken, map[string]interface{}{"value": p.value}) if w.Code != http.StatusOK { t.Fatalf("set policy %s: want 200, got %d: %s", p.key, w.Code, w.Body.String()) } } // Verify via ListGlobalSettings w := h.request("GET", "/api/v1/admin/settings", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("get settings: want 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) respPolicies, ok := resp["policies"].(map[string]interface{}) if !ok { t.Fatalf("settings response must have 'policies' map, got %v", resp) } if respPolicies["allow_user_byok"] != "true" { t.Errorf("allow_user_byok: want 'true', got %v", respPolicies["allow_user_byok"]) } if respPolicies["allow_user_personas"] != "true" { t.Errorf("allow_user_personas: want 'true', got %v", respPolicies["allow_user_personas"]) } } func TestIntegration_PublicSettingsExposePolicies(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Set a policy h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) // Public settings (no auth required) w := h.request("GET", "/api/v1/settings/public", "", nil) if w.Code != http.StatusOK { t.Fatalf("public settings: want 200, got %d", w.Code) } var resp map[string]interface{} decode(w, &resp) policies, ok := resp["policies"].(map[string]interface{}) if !ok { t.Fatalf("public settings must have 'policies', got %v", resp) } if policies["allow_user_byok"] != "true" { t.Errorf("public allow_user_byok: want 'true', got %v", policies["allow_user_byok"]) } if policies["allow_user_personas"] == nil { t.Error("public settings should expose allow_user_personas") } } // ── 4. Provider Configs (Admin) ────────────── func TestIntegration_AdminProviderConfigCRUD(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Create w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": "Test OpenAI", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-test123", }) if w.Code != http.StatusCreated { t.Fatalf("create config: want 201, got %d: %s", w.Code, w.Body.String()) } var created map[string]interface{} decode(w, &created) configID := created["id"].(string) // ── Verify API key is actually stored in DB ── var storedKey string err := database.TestDB.QueryRow( "SELECT api_key_enc FROM provider_configs WHERE id = $1", configID, ).Scan(&storedKey) if err != nil { t.Fatalf("query stored key: %v", err) } if storedKey != "sk-test123" { t.Fatalf("API key not stored: want 'sk-test123', got %q", storedKey) } // List w = h.request("GET", "/api/v1/admin/configs", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("list configs: want 200, got %d", w.Code) } // ── Update API key via PUT ── w = h.request("PUT", fmt.Sprintf("/api/v1/admin/configs/%s", configID), adminToken, map[string]interface{}{"api_key": "sk-updated456"}) if w.Code != http.StatusOK { t.Fatalf("update config: want 200, got %d: %s", w.Code, w.Body.String()) } // Verify the key was actually updated err = database.TestDB.QueryRow( "SELECT api_key_enc FROM provider_configs WHERE id = $1", configID, ).Scan(&storedKey) if err != nil { t.Fatalf("query updated key: %v", err) } if storedKey != "sk-updated456" { t.Fatalf("API key not updated: want 'sk-updated456', got %q", storedKey) } // Delete w = h.request("DELETE", fmt.Sprintf("/api/v1/admin/configs/%s", configID), adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("delete config: want 200, got %d: %s", w.Code, w.Body.String()) } } // TestIntegration_AdminProviderAPIKeyUsedByFetch verifies that the stored API // key is actually passed to the provider when fetching models. This catches // the json:"-" bug where CreateGlobalConfig silently dropped the API key. func TestIntegration_AdminProviderAPIKeyUsedByFetch(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Create provider with a deliberate bad key — we expect the fetch to // return an auth error FROM the provider, proving the key was sent. w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": "KeyTest", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-badkey-for-test", }) if w.Code != http.StatusCreated { t.Fatalf("create config: want 201, got %d: %s", w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Verify key stored in DB var storedKey string database.TestDB.QueryRow( "SELECT api_key_enc FROM provider_configs WHERE id = $1", configID, ).Scan(&storedKey) if storedKey != "sk-badkey-for-test" { t.Fatalf("key not stored: want 'sk-badkey-for-test', got %q — json:\"-\" bug is back", storedKey) } // Fetch models — this will fail (bad key) but the error message should // contain an auth/HTTP error from OpenAI, NOT a DNS or empty-key error. // We're not testing OpenAI connectivity here, just that the key reaches // the provider layer. w = h.request("POST", "/api/v1/admin/models/fetch", adminToken, map[string]interface{}{"provider_config_id": configID}) // Accept either 502 (single provider fetch fails) or 200 with errors array. // The key point: the handler TRIED to call the provider with our key. var fetchResp map[string]interface{} decode(w, &fetchResp) if w.Code == http.StatusOK { // Multi-provider path returns 200 with errors if errs, ok := fetchResp["errors"]; ok { errList := errs.([]interface{}) if len(errList) > 0 { errMsg := errList[0].(string) t.Logf(" Provider returned error (expected): %s", errMsg) } } } else if w.Code == http.StatusBadGateway { // Single-provider path returns 502 errMsg, _ := fetchResp["error"].(string) t.Logf(" Provider returned error (expected): %s", errMsg) } else { t.Fatalf("fetch models: unexpected status %d: %s", w.Code, w.Body.String()) } } // ── 5. Model Visibility / Resolution ───────── func TestIntegration_ModelVisibilityResolution(t *testing.T) { h := setupHarness(t) adminID, adminToken := h.createAdminUser("admin", "admin@test.com") _ = adminID // Create global provider config 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: want 201, got %d: %s", w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Insert a model into catalog directly (simulating fetch) _, err := database.TestDB.Exec(` INSERT INTO model_catalog (provider_config_id, model_id, display_name, visibility) VALUES ($1, 'gpt-4o', 'GPT-4o', 'disabled') `, configID) if err != nil { t.Fatalf("insert catalog entry: %v", err) } // As admin, models/enabled should return empty (model disabled) w = h.request("GET", "/api/v1/models/enabled", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("models/enabled: want 200, got %d: %s", w.Code, w.Body.String()) } var modelsResp map[string]interface{} decode(w, &modelsResp) modelsList := modelsResp["models"].([]interface{}) if len(modelsList) != 0 { t.Errorf("disabled model should not appear, got %d models", len(modelsList)) } // Enable the model var catalogID string database.TestDB.QueryRow("SELECT id FROM model_catalog WHERE model_id = 'gpt-4o' AND provider_config_id = $1", configID).Scan(&catalogID) w = h.request("PUT", fmt.Sprintf("/api/v1/admin/models/%s", catalogID), adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { t.Fatalf("enable model: want 200, got %d: %s", w.Code, w.Body.String()) } // Now models/enabled should return 1 model w = h.request("GET", "/api/v1/models/enabled", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("models/enabled after enable: want 200, got %d: %s", w.Code, w.Body.String()) } decode(w, &modelsResp) modelsList = modelsResp["models"].([]interface{}) if len(modelsList) != 1 { t.Errorf("enabled model should appear, want 1 got %d", len(modelsList)) } } // ── 6. Teams ───────────────────────────────── func TestIntegration_TeamMemberManagement(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Create regular user userID := database.SeedTestUser(t, "alice", "alice@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", userID) // Create team w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{ "name": "Engineering", "description": "Eng team", }) if w.Code != http.StatusCreated { t.Fatalf("create team: want 201, got %d: %s", w.Code, w.Body.String()) } var team map[string]interface{} decode(w, &team) teamID := team["id"].(string) // List users (verify response has 'users' field, not 'data') w = h.request("GET", "/api/v1/admin/users?page=1&per_page=200", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("list users: want 200, got %d", w.Code) } var usersResp map[string]interface{} decode(w, &usersResp) if _, ok := usersResp["users"]; !ok { t.Fatal("admin/users response MUST have 'users' key (not 'data')") } users := usersResp["users"].([]interface{}) if len(users) < 2 { t.Fatalf("expected at least 2 users (admin + alice), got %d", len(users)) } // Add member w = h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken, map[string]string{"user_id": userID, "role": "member"}) if w.Code != http.StatusOK && w.Code != http.StatusCreated { t.Fatalf("add member: want 200/201, got %d: %s", w.Code, w.Body.String()) } // List members w = h.request("GET", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("list members: want 200, got %d: %s", w.Code, w.Body.String()) } var membersResp map[string]interface{} decode(w, &membersResp) members := membersResp["data"].([]interface{}) if len(members) != 1 { t.Fatalf("expected 1 member, got %d", len(members)) } // Verify the user shows up in teams/mine userToken := makeToken(userID, "alice@test.com", "user") w = h.request("GET", "/api/v1/teams/mine", userToken, nil) if w.Code != http.StatusOK { t.Fatalf("teams/mine: want 200, got %d: %s", w.Code, w.Body.String()) } } // ── 7. Presets (Policy Gated) ──────────────── func TestIntegration_PresetCreation_PolicyGated(t *testing.T) { h := setupHarness(t) adminID, adminToken := h.createAdminUser("admin", "admin@test.com") _ = adminID // Create a provider config (presets need a valid model 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", }) var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Ensure allow_user_personas = false database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_personas', 'false') ON CONFLICT (key) DO UPDATE SET value = 'false'") // Try to create a preset as admin (role=admin but policy says no) w = h.request("POST", "/api/v1/presets", adminToken, map[string]interface{}{ "name": "My Preset", "base_model_id": "gpt-4o", "provider_config_id": configID, "system_prompt": "You are helpful", }) if w.Code != http.StatusForbidden { t.Fatalf("preset create with policy=false: want 403, got %d: %s", w.Code, w.Body.String()) } // Enable the policy h.request("PUT", "/api/v1/admin/settings/allow_user_personas", adminToken, map[string]interface{}{"value": "true"}) // Now creation should succeed w = h.request("POST", "/api/v1/presets", adminToken, map[string]interface{}{ "name": "My Preset", "base_model_id": "gpt-4o", "provider_config_id": configID, "system_prompt": "You are helpful", }) if w.Code != http.StatusCreated { t.Fatalf("preset create with policy=true: want 201, got %d: %s", w.Code, w.Body.String()) } // List presets w = h.request("GET", "/api/v1/presets", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("list presets: want 200, got %d: %s", w.Code, w.Body.String()) } } // ── 8. User Provider BYOK (Policy Gated) ──── func TestIntegration_UserBYOK_PolicyGated(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Ensure allow_user_byok = false database.TestDB.Exec("INSERT INTO platform_policies (key, value) VALUES ('allow_user_byok', 'false') ON CONFLICT (key) DO UPDATE SET value = 'false'") 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("bob", "bob@test.com", "password123") // Try to create a personal provider — should fail w := h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{ "name": "MyKey", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-user123", }) if w.Code == http.StatusCreated { t.Fatal("BYOK create should be blocked when allow_user_byok=false") } // Enable BYOK h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) // Now should succeed w = h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{ "name": "MyKey", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-user123", }) if w.Code != http.StatusCreated { t.Fatalf("BYOK create with policy=true: want 201, got %d: %s", w.Code, w.Body.String()) } var created map[string]interface{} decode(w, &created) cfgID := created["id"].(string) // List should only show personal configs, NOT global ones w = h.request("GET", "/api/v1/api-configs", userToken, nil) if w.Code != http.StatusOK { t.Fatalf("list configs: want 200, got %d", w.Code) } // Update w = h.request("PUT", fmt.Sprintf("/api/v1/api-configs/%s", cfgID), userToken, map[string]interface{}{"name": "MyKey-Updated"}) if w.Code != http.StatusOK { t.Fatalf("update config: want 200, got %d: %s", w.Code, w.Body.String()) } // Delete w = h.request("DELETE", fmt.Sprintf("/api/v1/api-configs/%s", cfgID), userToken, nil) if w.Code != http.StatusOK { t.Fatalf("delete config: want 200, got %d: %s", w.Code, w.Body.String()) } } // ── 9. Notes CRUD ──────────────────────────── func TestIntegration_NotesCRUD(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Create w := h.request("POST", "/api/v1/notes", adminToken, map[string]interface{}{ "title": "Test Note", "content": "Hello world", "folder": "general", }) if w.Code != http.StatusCreated { t.Fatalf("create note: want 201, got %d: %s", w.Code, w.Body.String()) } var note map[string]interface{} decode(w, ¬e) noteID := note["id"].(string) // Get w = h.request("GET", fmt.Sprintf("/api/v1/notes/%s", noteID), adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("get note: want 200, got %d", w.Code) } // Update w = h.request("PUT", fmt.Sprintf("/api/v1/notes/%s", noteID), adminToken, map[string]interface{}{"title": "Updated", "content": "Updated content"}) if w.Code != http.StatusOK { t.Fatalf("update note: want 200, got %d: %s", w.Code, w.Body.String()) } // List w = h.request("GET", "/api/v1/notes", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("list notes: want 200, got %d", w.Code) } // Delete w = h.request("DELETE", fmt.Sprintf("/api/v1/notes/%s", noteID), adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("delete note: want 200, got %d", w.Code) } } // ── 10. Cross-User Isolation ───────────────── func TestIntegration_CrossUserIsolation(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'") _, userAToken := h.registerUser("alice", "alice@test.com", "password123") _, userBToken := h.registerUser("charlie", "charlie@test.com", "password123") // Alice creates a provider w := h.request("POST", "/api/v1/api-configs", userAToken, map[string]interface{}{ "name": "AliceKey", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-alice", }) if w.Code != http.StatusCreated { t.Fatalf("alice create config: want 201, got %d: %s", w.Code, w.Body.String()) } var aliceCfg map[string]interface{} decode(w, &aliceCfg) aliceCfgID := aliceCfg["id"].(string) // Bob should NOT see Alice's provider w = h.request("GET", "/api/v1/api-configs", userBToken, nil) if w.Code != http.StatusOK { t.Fatalf("bob list configs: want 200, got %d", w.Code) } // Bob should NOT be able to delete Alice's provider w = h.request("DELETE", fmt.Sprintf("/api/v1/api-configs/%s", aliceCfgID), userBToken, nil) if w.Code != http.StatusForbidden { t.Fatalf("bob delete alice's config: want 403, got %d", w.Code) } // Admin should NOT see personal providers in admin list (they're in /admin/configs for global only) _ = adminToken } // ── 9. Admin Model Fetch → Enable → User Visibility ───── func TestIntegration_AdminModelFetchEnableUserSees(t *testing.T) { h := setupHarness(t) adminID, adminToken := h.createAdminUser("admin", "admin@test.com") _ = adminID // Create global provider config 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: want 201, got %d: %s", w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Insert models directly (simulating successful provider fetch) for _, mid := range []string{"gpt-4o", "gpt-4o-mini", "o1-preview"} { _, err := database.TestDB.Exec(` INSERT INTO model_catalog (provider_config_id, model_id, display_name, capabilities, visibility) VALUES ($1, $2, $3, '{"streaming":true,"tool_calling":true}'::jsonb, 'disabled') `, configID, mid, mid) if err != nil { t.Fatalf("insert %s: %v", mid, err) } } // ── Admin list should show ALL models (including disabled) ── w = h.request("GET", "/api/v1/admin/models", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("admin list models: want 200, got %d: %s", w.Code, w.Body.String()) } var adminResp map[string]interface{} decode(w, &adminResp) adminModels := adminResp["models"].([]interface{}) if len(adminModels) != 3 { t.Fatalf("admin should see 3 disabled models, got %d", len(adminModels)) } // Verify admin response is non-null array (not {"models": null}) if adminResp["models"] == nil { t.Fatal("admin models must be [] not null — causes frontend fallback chain to break") } // ── User should see 0 models (all disabled) ── userID := database.SeedTestUser(t, "testuser", "user@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", userID) userToken := makeToken(userID, "user@test.com", "user") w = h.request("GET", "/api/v1/models/enabled", userToken, nil) if w.Code != http.StatusOK { t.Fatalf("user models/enabled: want 200, got %d: %s", w.Code, w.Body.String()) } var userResp map[string]interface{} decode(w, &userResp) userModels := userResp["models"].([]interface{}) if len(userModels) != 0 { t.Errorf("disabled models must not appear for user, got %d", len(userModels)) } // Verify user response is non-null array if userResp["models"] == nil { t.Fatal("user models must be [] not null — causes '📋 Loaded 0 models' to crash") } // ── Admin enables one model ── var catalogID string database.TestDB.QueryRow( "SELECT id FROM model_catalog WHERE model_id = 'gpt-4o' AND provider_config_id = $1", configID, ).Scan(&catalogID) w = h.request("PUT", fmt.Sprintf("/api/v1/admin/models/%s", catalogID), adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { t.Fatalf("enable model: want 200, got %d: %s", w.Code, w.Body.String()) } // ── User should now see 1 model ── w = h.request("GET", "/api/v1/models/enabled", userToken, nil) if w.Code != http.StatusOK { t.Fatalf("user models/enabled after enable: want 200, got %d: %s", w.Code, w.Body.String()) } decode(w, &userResp) userModels = userResp["models"].([]interface{}) if len(userModels) != 1 { t.Fatalf("user should see 1 enabled model, got %d", len(userModels)) } // ── Validate response shape matches frontend contract ── m := userModels[0].(map[string]interface{}) // Backend MUST send provider_config_id (Go struct canonical) if m["provider_config_id"] == nil || m["provider_config_id"] == "" { t.Error("MISSING: provider_config_id — Go struct canonical field") } // Backend MUST send config_id alias (frontend reads this) if m["config_id"] == nil || m["config_id"] == "" { t.Error("MISSING: config_id — frontend alias, composite IDs will break") } // Alias must match canonical if m["config_id"] != m["provider_config_id"] { t.Errorf("config_id (%v) must equal provider_config_id (%v)", m["config_id"], m["provider_config_id"]) } // model_id required for composite ID construction if m["model_id"] == nil || m["model_id"] == "" { t.Error("MISSING: model_id — required for composite ID") } // source required for preset detection if m["source"] == nil || m["source"] == "" { t.Error("MISSING: source — frontend uses this to distinguish catalog vs persona") } // provider_name required for display if m["provider_name"] == nil || m["provider_name"] == "" { t.Error("MISSING: provider_name — frontend model selector display") } // capabilities must be object not null if m["capabilities"] == nil { t.Error("capabilities must not be null") } } // ═══════════════════════════════════════════════════ // USER JOURNEY TESTS — API calls only // ═══════════════════════════════════════════════════ // These tests exercise the ACTUAL user experience. // No insertModel(). No insertProvider(). Only API calls. // // The ONLY raw SQL allowed is labeled [SIMULATED FETCH] // to represent what an external provider API would return, // since integration tests can't hit real OpenAI/Venice APIs. // ═══════════════════════════════════════════════════ // ── Test helpers ── // getModels calls GET /models/enabled and returns the model list. func (h *testHarness) getModels(token string) []interface{} { h.t.Helper() w := h.request("GET", "/api/v1/models/enabled", token, nil) if w.Code != http.StatusOK { h.t.Fatalf("models/enabled: want 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} decode(w, &resp) if resp["models"] == nil { h.t.Fatal("models response must never be null") } return resp["models"].([]interface{}) } func getModelIDs(models []interface{}) []string { ids := make([]string, 0, len(models)) for _, raw := range models { m := raw.(map[string]interface{}) if mid, ok := m["model_id"].(string); ok { ids = append(ids, mid) } } return ids } func hasModel(models []interface{}, modelID string) bool { for _, raw := range models { m := raw.(map[string]interface{}) if m["model_id"] == modelID { return true } } return false } func hasModelWithScope(models []interface{}, modelID, scope string) bool { for _, raw := range models { m := raw.(map[string]interface{}) if m["model_id"] == modelID && m["scope"] == scope { return true } } return false } // simulateFetch inserts models into model_catalog as if a provider API returned them. // This is the ONLY raw SQL in journey tests — clearly labeled because integration // tests cannot hit real external APIs (OpenAI, Venice, etc). func simulateFetch(t *testing.T, providerConfigID string, models []string, visibility string) { t.Helper() for _, modelID := range models { _, err := database.TestDB.Exec(` INSERT INTO model_catalog (provider_config_id, model_id, display_name, visibility) VALUES ($1, $2, $3, $4) `, providerConfigID, modelID, modelID, visibility) if err != nil { t.Fatalf("[SIMULATED FETCH] insert %s: %v", modelID, err) } } } // ── Journey 1: Admin creates provider → user sees models ── func TestUserJourney_AdminProvider_UserSeesModels(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Regular user — no special role, no team userID := database.SeedTestUser(t, "alice", "alice@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", userID) userToken := makeToken(userID, "alice@test.com", "user") // Step 1: Admin creates provider via API w := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": "TestOpenAI", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-test", }) if w.Code != http.StatusCreated { t.Fatalf("admin create config: want 201, got %d: %s", w.Code, w.Body.String()) } var cfg map[string]interface{} decode(w, &cfg) configID := cfg["id"].(string) // Step 2: [SIMULATED FETCH] — represents POST /admin/models/fetch hitting OpenAI API // In production: admin clicks "Fetch Models" → backend calls OpenAI → inserts into catalog // In test: we insert directly because we can't call a real API simulateFetch(t, configID, []string{"gpt-4o", "gpt-4o-mini", "gpt-3.5-turbo"}, "disabled") // Step 3: User should see 0 models (all disabled) userModels := h.getModels(userToken) if len(userModels) != 0 { t.Fatalf("before admin enables: user should see 0 models, got %d", len(userModels)) } // Step 4: Admin enables one model via API var catalogID string database.TestDB.QueryRow( "SELECT id FROM model_catalog WHERE model_id = 'gpt-4o' AND provider_config_id = $1", configID, ).Scan(&catalogID) w = h.request("PUT", "/api/v1/admin/models/"+catalogID, adminToken, map[string]interface{}{"visibility": "enabled"}) if w.Code != http.StatusOK { t.Fatalf("admin enable model: want 200, got %d: %s", w.Code, w.Body.String()) } // Step 5: User should now see exactly 1 model userModels = h.getModels(userToken) if len(userModels) != 1 { t.Fatalf("after admin enables gpt-4o: user should see 1 model, got %d: %v", len(userModels), getModelIDs(userModels)) } // Step 6: Verify the model has all required frontend fields m := userModels[0].(map[string]interface{}) if m["model_id"] != "gpt-4o" { t.Errorf("expected model_id=gpt-4o, got %v", m["model_id"]) } if m["config_id"] == nil || m["config_id"] == "" { t.Error("MISSING config_id — frontend needs this for composite model ID") } if m["provider_name"] == nil || m["provider_name"] == "" { t.Error("MISSING provider_name — frontend model selector display") } if m["scope"] != "global" { t.Errorf("expected scope=global, got %v", m["scope"]) } } // ── Journey 2: User creates BYOK provider → auto-fetch triggers ── // // After the fix in apiconfigs.go, CreateConfig now auto-fetches models // from the provider API and auto-enables them. In integration tests, // the real API isn't reachable so we get a warning — but the provider // is still created (201). The Live Venice test validates the real flow. func TestUserJourney_BYOK_AutoFetchTriggered(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Enable BYOK policy h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) // Regular user 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("bob", "bob@test.com", "password123") // Step 1: User creates BYOK provider via API w := h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{ "name": "My OpenAI Key", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-fake-key", }) if w.Code != http.StatusCreated { t.Fatalf("create BYOK provider: want 201, got %d: %s", w.Code, w.Body.String()) } // Step 2: Response should include the provider ID and a fetch result. // In integration tests, the real API isn't reachable, so we expect: // - id: present (provider was created) // - warning: present (fetch failed — no real API in test) // - models_fetched: 0 (or absent) var created map[string]interface{} decode(w, &created) cfgID := created["id"].(string) if cfgID == "" { t.Fatal("provider creation should return an id") } // Warning is expected in integration tests (can't reach real OpenAI API) if created["warning"] != nil { t.Logf("expected warning in test env: %v", created["warning"]) } // Step 3: models/enabled → 0 personal models (fetch failed, catalog empty) // This is CORRECT behavior in test env. Live Venice test validates real flow. userModels := h.getModels(userToken) personalCount := 0 for _, raw := range userModels { m := raw.(map[string]interface{}) if m["scope"] == "personal" { personalCount++ } } t.Logf("personal models after auto-fetch (test env, no real API): %d", personalCount) } // ── Journey 3: BYOK models exist in catalog but NULL scan kills them ── // // Even if we manually populate the catalog for a BYOK provider // (simulating what auto-fetch WOULD do), the provider scan fails // because model_default is NULL and scanProviders uses bare string. // // This test FAILS before the provider.go fix, PASSES after. func TestUserJourney_BYOK_NullScanKillsModels(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Enable BYOK h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) 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("alice", "alice@test.com", "password123") // User creates BYOK provider via API // Use unreachable endpoint so auto-fetch fails — simulateFetch controls the catalog. w := h.request("POST", "/api/v1/api-configs", userToken, map[string]interface{}{ "name": "My Venice", "provider": "venice", "endpoint": "http://localhost:1/v1", "api_key": "sk-alice-key", }) if w.Code != http.StatusCreated { t.Fatalf("create BYOK: want 201, got %d: %s", w.Code, w.Body.String()) } var created map[string]interface{} decode(w, &created) cfgID := created["id"].(string) // [SIMULATED FETCH] — what auto-fetch SHOULD do after provider creation // Inserts models with visibility='enabled' (BYOK models should be auto-enabled) simulateFetch(t, cfgID, []string{"llama-3.3-70b", "deepseek-r1"}, "enabled") // User calls models/enabled — should see their 2 personal models userModels := h.getModels(userToken) personalModels := 0 for _, raw := range userModels { m := raw.(map[string]interface{}) if m["scope"] == "personal" { personalModels++ } } // Before provider.go NULL scan fix: personalModels = 0 (scan crashes, models silently lost) // After fix: personalModels = 2 if personalModels != 2 { t.Fatalf("user should see 2 personal BYOK models, got %d\n"+ " If 0: provider.go scanProviders crashes on NULL model_default\n"+ " Check: warn: failed to load personal providers: sql: Scan error", personalModels) } // Verify the models have correct scope and fields if !hasModelWithScope(userModels, "llama-3.3-70b", "personal") { t.Error("llama-3.3-70b should appear with scope=personal") } if !hasModelWithScope(userModels, "deepseek-r1", "personal") { t.Error("deepseek-r1 should appear with scope=personal") } } // ── Journey 4: Team provider → member sees, non-member doesn't ── func TestUserJourney_TeamProvider_MemberVsNonMember(t *testing.T) { h := setupHarness(t) _, adminToken := h.createAdminUser("admin", "admin@test.com") // Create team members aliceID := database.SeedTestUser(t, "alice", "alice@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", aliceID) aliceToken := makeToken(aliceID, "alice@test.com", "user") bobID := database.SeedTestUser(t, "bob", "bob@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", bobID) bobToken := makeToken(bobID, "bob@test.com", "user") // Step 1: Admin creates team via API w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{ "name": "Engineering", "description": "Eng team", }) if w.Code != http.StatusCreated { t.Fatalf("create team: %d: %s", w.Code, w.Body.String()) } var team map[string]interface{} decode(w, &team) teamID := team["id"].(string) // Step 2: Admin adds alice as team admin, bob is NOT added h.request("POST", fmt.Sprintf("/api/v1/admin/teams/%s/members", teamID), adminToken, map[string]string{"user_id": aliceID, "role": "admin"}) // Step 3: Team admin (alice) creates team provider via self-service API w = h.request("POST", fmt.Sprintf("/api/v1/teams/%s/providers", teamID), aliceToken, map[string]interface{}{ "name": "Team Venice", "provider": "venice", "endpoint": "https://api.venice.ai/api/v1", "api_key": "sk-team-key", }) if w.Code != http.StatusCreated { t.Fatalf("create team provider: want 201, got %d: %s", w.Code, w.Body.String()) } var prov map[string]interface{} decode(w, &prov) provID := prov["id"].(string) // Step 4: [SIMULATED FETCH] — what fetching from Venice API would return simulateFetch(t, provID, []string{"llama-3.3-70b", "deepseek-r1"}, "enabled") // Step 5: Also add a global model so we can verify additive behavior gw := h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": "GlobalOpenAI", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-global", }) var gcfg map[string]interface{} decode(gw, &gcfg) globalCfgID := gcfg["id"].(string) simulateFetch(t, globalCfgID, []string{"gpt-4o"}, "enabled") // Step 6: Alice (team member) sees global + team models aliceModels := h.getModels(aliceToken) if !hasModel(aliceModels, "gpt-4o") { t.Error("alice should see global model gpt-4o") } if !hasModel(aliceModels, "llama-3.3-70b") { t.Errorf("alice (team member) should see team model llama-3.3-70b, got: %v", getModelIDs(aliceModels)) } // Step 7: Bob (NOT in team) sees ONLY global models bobModels := h.getModels(bobToken) if !hasModel(bobModels, "gpt-4o") { t.Error("bob should see global model gpt-4o") } if hasModel(bobModels, "llama-3.3-70b") { t.Error("bob (non-member) should NOT see team model llama-3.3-70b") } if hasModel(bobModels, "deepseek-r1") { t.Error("bob (non-member) should NOT see team model deepseek-r1") } } // ── Journey 5: Full 4-actor matrix ── // // All providers created via API. All assertions via API. // Only raw SQL is [SIMULATED FETCH]. // // Actors: // platformAdmin — system admin, NOT in any team // teamAdmin — team_members.role='admin' in Engineering // teamMember — team_members.role='member' in Engineering // outsider — regular user, no team // // Expected: // | Actor | Global(en) | Team(en) | BYOK(own) | Total | // |--------------|------------|----------|-----------|-------| // | platformAdmin| 2 | 0 | 0 | 2 | // | teamAdmin | 2 | 1 | 0 | 3 | // | teamMember | 2 | 1 | 1 * | 4 | // | outsider | 2 | 0 | 1 * | 3 | // // * BYOK models require provider.go NULL scan fix func TestUserJourney_FullMatrix(t *testing.T) { h := setupHarness(t) adminID, adminToken := h.createAdminUser("platformadmin", "platformadmin@test.com") _ = adminID // Create all actors teamAdminID := database.SeedTestUser(t, "teamadmin", "teamadmin@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", teamAdminID) teamAdminToken := makeToken(teamAdminID, "teamadmin@test.com", "user") teamMemberID := database.SeedTestUser(t, "teammember", "teammember@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", teamMemberID) teamMemberToken := makeToken(teamMemberID, "teammember@test.com", "user") outsiderID := database.SeedTestUser(t, "outsider", "outsider@test.com") database.TestDB.Exec("UPDATE users SET is_active = true WHERE id = $1", outsiderID) outsiderToken := makeToken(outsiderID, "outsider@test.com", "user") // ── Setup: Enable BYOK policy ── h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) // ── Setup: Create team via admin API ── w := h.request("POST", "/api/v1/admin/teams", adminToken, map[string]string{ "name": "Engineering", "description": "Eng team", }) if w.Code != http.StatusCreated { t.Fatalf("create team: %d", w.Code) } var team map[string]interface{} decode(w, &team) teamID := team["id"].(string) // Add teamAdmin and teamMember to team (platformAdmin and outsider are NOT added) 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": teamMemberID, "role": "member"}) // ── Setup: Global provider via admin API ── w = h.request("POST", "/api/v1/admin/configs", adminToken, map[string]interface{}{ "name": "GlobalOpenAI", "provider": "openai", "endpoint": "https://api.openai.com/v1", "api_key": "sk-global", }) if w.Code != http.StatusCreated { t.Fatalf("create global config: %d", w.Code) } var gcfg map[string]interface{} decode(w, &gcfg) globalCfgID := gcfg["id"].(string) // [SIMULATED FETCH] for global provider: 2 enabled + 1 disabled simulateFetch(t, globalCfgID, []string{"gpt-4o", "gpt-4o-mini"}, "enabled") simulateFetch(t, globalCfgID, []string{"gpt-3.5-turbo"}, "disabled") // ── Setup: Team provider via team admin self-service API ── w = h.request("POST", fmt.Sprintf("/api/v1/teams/%s/providers", teamID), teamAdminToken, map[string]interface{}{ "name": "TeamVenice", "provider": "venice", "endpoint": "https://api.venice.ai/api/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) // [SIMULATED FETCH] for team provider: 1 enabled + 1 disabled simulateFetch(t, teamProvID, []string{"llama-3.3-70b"}, "enabled") simulateFetch(t, teamProvID, []string{"deepseek-r1"}, "disabled") // ── Setup: BYOK providers via user API ── // Use unreachable endpoints so auto-fetch fails — simulateFetch controls catalog. w = h.request("POST", "/api/v1/api-configs", teamMemberToken, map[string]interface{}{ "name": "MemberKey", "provider": "openai", "endpoint": "http://localhost:1/v1", "api_key": "sk-member", }) if w.Code != http.StatusCreated { t.Fatalf("create member BYOK: %d: %s", w.Code, w.Body.String()) } var memberCfg map[string]interface{} decode(w, &memberCfg) memberBYOKID := memberCfg["id"].(string) // [SIMULATED FETCH] for member BYOK simulateFetch(t, memberBYOKID, []string{"gpt-4o-member-byok"}, "enabled") w = h.request("POST", "/api/v1/api-configs", outsiderToken, map[string]interface{}{ "name": "OutsiderKey", "provider": "venice", "endpoint": "http://localhost:1/v1", "api_key": "sk-outsider", }) if w.Code != http.StatusCreated { t.Fatalf("create outsider BYOK: %d: %s", w.Code, w.Body.String()) } var outsiderCfg map[string]interface{} decode(w, &outsiderCfg) outsiderBYOKID := outsiderCfg["id"].(string) // [SIMULATED FETCH] for outsider BYOK simulateFetch(t, outsiderBYOKID, []string{"llama-outsider-byok"}, "enabled") // ════════════════════════════════════════════ // ASSERTIONS — what each actor actually sees // ════════════════════════════════════════════ t.Run("platformAdmin_sees_global_only", func(t *testing.T) { models := h.getModels(adminToken) ids := getModelIDs(models) if len(models) != 2 { t.Fatalf("platformAdmin: want 2 (global enabled), got %d: %v", len(models), ids) } if !hasModel(models, "gpt-4o") || !hasModel(models, "gpt-4o-mini") { t.Errorf("platformAdmin should see gpt-4o and gpt-4o-mini, got: %v", ids) } if hasModel(models, "gpt-3.5-turbo") { t.Error("platformAdmin should NOT see disabled gpt-3.5-turbo") } if hasModel(models, "llama-3.3-70b") { t.Error("platformAdmin should NOT see team model (not in team)") } }) t.Run("teamAdmin_sees_global_plus_team", func(t *testing.T) { models := h.getModels(teamAdminToken) ids := getModelIDs(models) if len(models) != 3 { t.Fatalf("teamAdmin: want 3 (2 global + 1 team), got %d: %v", len(models), ids) } if !hasModel(models, "llama-3.3-70b") { t.Errorf("teamAdmin should see team model llama-3.3-70b, got: %v", ids) } if hasModel(models, "deepseek-r1") { t.Error("teamAdmin should NOT see disabled team model deepseek-r1") } }) t.Run("teamMember_sees_global_plus_team_plus_byok", func(t *testing.T) { models := h.getModels(teamMemberToken) ids := getModelIDs(models) if len(models) != 4 { t.Fatalf("teamMember: want 4 (2 global + 1 team + 1 BYOK), got %d: %v\n"+ " If 3: provider.go NULL scan bug is hiding BYOK models\n"+ " If 2: team provider scan also failing", len(models), ids) } if !hasModelWithScope(models, "gpt-4o-member-byok", "personal") { t.Error("teamMember should see own BYOK model with scope=personal") } if hasModel(models, "llama-outsider-byok") { t.Error("teamMember should NOT see outsider's BYOK model") } }) t.Run("outsider_sees_global_plus_own_byok", func(t *testing.T) { models := h.getModels(outsiderToken) ids := getModelIDs(models) if len(models) != 3 { t.Fatalf("outsider: want 3 (2 global + 1 BYOK), got %d: %v\n"+ " If 2: provider.go NULL scan bug is hiding BYOK models", len(models), ids) } if !hasModelWithScope(models, "llama-outsider-byok", "personal") { t.Error("outsider should see own BYOK model with scope=personal") } if hasModel(models, "llama-3.3-70b") { t.Error("outsider should NOT see team model (not in team)") } if hasModel(models, "gpt-4o-member-byok") { t.Error("outsider should NOT see teamMember's BYOK model") } }) // ── Dynamic state changes ── t.Run("byok_policy_off_hides_personal_models", func(t *testing.T) { h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "false"}) defer h.request("PUT", "/api/v1/admin/settings/allow_user_byok", adminToken, map[string]interface{}{"value": "true"}) models := h.getModels(teamMemberToken) if hasModelWithScope(models, "gpt-4o-member-byok", "personal") { t.Error("BYOK off: teamMember should NOT see personal models") } if len(models) != 3 { t.Fatalf("BYOK off: teamMember want 3 (2 global + 1 team), got %d: %v", len(models), getModelIDs(models)) } models = h.getModels(outsiderToken) if len(models) != 2 { t.Fatalf("BYOK off: outsider want 2 (global only), got %d: %v", len(models), getModelIDs(models)) } }) t.Run("admin_disables_global_model_users_lose_it", func(t *testing.T) { var catalogID string database.TestDB.QueryRow( "SELECT id FROM model_catalog WHERE model_id = 'gpt-4o' AND provider_config_id = $1", globalCfgID, ).Scan(&catalogID) // Admin disables gpt-4o w := h.request("PUT", "/api/v1/admin/models/"+catalogID, adminToken, map[string]interface{}{"visibility": "disabled"}) if w.Code != http.StatusOK { t.Fatalf("disable model: %d", w.Code) } defer func() { h.request("PUT", "/api/v1/admin/models/"+catalogID, adminToken, map[string]interface{}{"visibility": "enabled"}) }() // Everyone loses gpt-4o for _, tc := range []struct { name string token string }{ {"platformAdmin", adminToken}, {"teamAdmin", teamAdminToken}, {"teamMember", teamMemberToken}, {"outsider", outsiderToken}, } { models := h.getModels(tc.token) if hasModel(models, "gpt-4o") { t.Errorf("%s should NOT see disabled gpt-4o", tc.name) } } }) t.Run("admin_models_shows_global_only", func(t *testing.T) { w := h.request("GET", "/api/v1/admin/models", adminToken, nil) if w.Code != http.StatusOK { t.Fatalf("admin/models: %d", w.Code) } var resp map[string]interface{} decode(w, &resp) allModels := resp["models"].([]interface{}) // Admin should see only global-scope models (3), not team(2) or BYOK(2) if len(allModels) != 3 { t.Fatalf("admin/models should show 3 global entries, got %d", len(allModels)) } }) }