This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/model_prefs_test.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

364 lines
11 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"testing"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"switchboard-core/config"
"switchboard-core/database"
"switchboard-core/middleware"
"switchboard-core/store"
postgres "switchboard-core/store/postgres"
sqlite "switchboard-core/store/sqlite"
)
// ── Model Prefs Test Harness ──────────────
type modelPrefsHarness struct {
*testHarness
userToken string
userID string
user2Token string
user2ID string
configID string // seeded provider_config for preference tests
}
func setupModelPrefsHarness(t *testing.T) *modelPrefsHarness {
t.Helper()
database.RequireTestDB(t)
database.TruncateAll(t)
cfg := &config.Config{
JWTSecret: testJWTSecret,
BasePath: "",
}
var stores store.Stores
if database.IsSQLite() {
stores = sqlite.NewStores(database.TestDB)
} else {
stores = postgres.NewStores(database.TestDB)
}
userCache := middleware.NewUserStatusCache()
r := gin.New()
api := r.Group("/api/v1")
protected := api.Group("")
protected.Use(middleware.Auth(cfg, stores.Users, userCache))
modelPrefs := NewModelPrefsHandler(stores)
protected.GET("/models/preferences", modelPrefs.GetPreferences)
protected.PUT("/models/preferences", modelPrefs.SetPreference)
protected.POST("/models/preferences/bulk", modelPrefs.BulkSetPreferences)
// Seed two users
userID := database.SeedTestUser(t, "prefuser", "prefuser@test.com")
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), userID)
userToken := makeToken(userID, "prefuser@test.com", "user")
user2ID := database.SeedTestUser(t, "prefuser2", "prefuser2@test.com")
database.TestDB.Exec(dialectSQL("UPDATE users SET is_active = true WHERE id = $1"), user2ID)
user2Token := makeToken(user2ID, "prefuser2@test.com", "user")
// Seed a provider config for preference targets
configID := uuid.New().String()
if database.IsSQLite() {
database.TestDB.Exec(`
INSERT INTO provider_configs (id, scope, name, provider, endpoint, key_scope)
VALUES (?, 'global', 'Test Provider', 'openai', 'https://api.openai.com/v1', 'global')`,
configID)
} else {
database.TestDB.Exec(`
INSERT INTO provider_configs (id, scope, name, provider, endpoint, key_scope)
VALUES ($1, 'global', 'Test Provider', 'openai', 'https://api.openai.com/v1', 'global')`,
configID)
}
return &modelPrefsHarness{
testHarness: &testHarness{router: r, t: t},
userToken: userToken,
userID: userID,
user2Token: user2Token,
user2ID: user2ID,
configID: configID,
}
}
// ── GET /models/preferences — empty state ─
func TestModelPrefs_Get_Empty(t *testing.T) {
h := setupModelPrefsHarness(t)
resp := h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("got %d, body: %s", resp.Code, resp.Body.String())
}
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
data, ok := body["data"]
if !ok {
t.Fatal("response must have 'data' key")
}
arr, ok := data.([]interface{})
if !ok {
t.Fatal("data must be an array")
}
if len(arr) != 0 {
t.Fatalf("expected empty array, got %d items", len(arr))
}
}
// ── PUT + GET round-trip ──────────────────
func TestModelPrefs_Set_RoundTrip(t *testing.T) {
h := setupModelPrefsHarness(t)
// Set preference
resp := h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "test-model-1",
"provider_config_id": h.configID,
"hidden": true,
"sort_order": 5,
})
if resp.Code != http.StatusOK {
t.Fatalf("PUT: got %d, body: %s", resp.Code, resp.Body.String())
}
// Read back
resp = h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("GET: got %d", resp.Code)
}
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 1 {
t.Fatalf("expected 1 preference, got %d", len(arr))
}
pref := arr[0].(map[string]interface{})
if pref["model_id"] != "test-model-1" {
t.Errorf("model_id: got %v", pref["model_id"])
}
if pref["provider_config_id"] != h.configID {
t.Errorf("provider_config_id: got %v, want %s", pref["provider_config_id"], h.configID)
}
if pref["hidden"] != true {
t.Errorf("hidden: got %v, want true", pref["hidden"])
}
if pref["sort_order"] != float64(5) {
t.Errorf("sort_order: got %v, want 5", pref["sort_order"])
}
}
// ── Upsert — no duplicate rows ────────────
func TestModelPrefs_Upsert_NoDuplicate(t *testing.T) {
h := setupModelPrefsHarness(t)
for _, hidden := range []bool{true, false, true} {
resp := h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "test-model-1",
"provider_config_id": h.configID,
"hidden": hidden,
})
if resp.Code != http.StatusOK {
t.Fatalf("PUT hidden=%v: got %d", hidden, resp.Code)
}
}
resp := h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 1 {
t.Fatalf("upsert produced %d rows, want 1", len(arr))
}
pref := arr[0].(map[string]interface{})
if pref["hidden"] != true {
t.Errorf("final hidden state should be true, got %v", pref["hidden"])
}
}
// ── Validation: missing fields ────────────
func TestModelPrefs_Set_MissingModelID(t *testing.T) {
h := setupModelPrefsHarness(t)
resp := h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"provider_config_id": h.configID,
"hidden": true,
})
if resp.Code != http.StatusBadRequest {
t.Fatalf("missing model_id: want 400, got %d", resp.Code)
}
}
func TestModelPrefs_Set_MissingProviderConfigID(t *testing.T) {
h := setupModelPrefsHarness(t)
resp := h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "test-model-1",
"hidden": true,
})
if resp.Code != http.StatusBadRequest {
t.Fatalf("missing provider_config_id: want 400, got %d", resp.Code)
}
}
// ── Bulk set hidden ───────────────────────
func TestModelPrefs_BulkSetHidden(t *testing.T) {
h := setupModelPrefsHarness(t)
resp := h.request("POST", "/api/v1/models/preferences/bulk", h.userToken, map[string]interface{}{
"entries": []map[string]string{
{"model_id": "bulk-model-a", "provider_config_id": h.configID},
{"model_id": "bulk-model-b", "provider_config_id": h.configID},
},
"hidden": true,
})
if resp.Code != http.StatusOK {
t.Fatalf("bulk: got %d, body: %s", resp.Code, resp.Body.String())
}
var bulkResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&bulkResp)
if bulkResp["count"] != float64(2) {
t.Errorf("bulk count: got %v, want 2", bulkResp["count"])
}
// Verify both are hidden
resp = h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
hiddenCount := 0
for _, raw := range arr {
p := raw.(map[string]interface{})
if p["hidden"] == true {
hiddenCount++
}
}
if hiddenCount < 2 {
t.Errorf("expected at least 2 hidden prefs, got %d", hiddenCount)
}
}
// ── User isolation ────────────────────────
func TestModelPrefs_UserIsolation(t *testing.T) {
h := setupModelPrefsHarness(t)
// User 1 sets a preference
resp := h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "isolated-model",
"provider_config_id": h.configID,
"hidden": true,
})
if resp.Code != http.StatusOK {
t.Fatalf("user1 PUT: got %d", resp.Code)
}
// User 2 should see empty preferences
resp = h.request("GET", "/api/v1/models/preferences", h.user2Token, nil)
if resp.Code != http.StatusOK {
t.Fatalf("user2 GET: got %d", resp.Code)
}
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) != 0 {
t.Fatalf("user2 should have 0 prefs, got %d", len(arr))
}
}
// ── Shape validation ──────────────────────
func TestModelPrefs_ResponseShape(t *testing.T) {
h := setupModelPrefsHarness(t)
// Create a preference so we have something to inspect
h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "shape-model",
"provider_config_id": h.configID,
"hidden": false,
"sort_order": 0,
})
resp := h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
if len(arr) == 0 {
t.Fatal("expected at least 1 preference for shape check")
}
pref := arr[0].(map[string]interface{})
requiredFields := []string{"id", "user_id", "model_id", "provider_config_id", "hidden", "sort_order", "created_at", "updated_at"}
for _, f := range requiredFields {
if _, ok := pref[f]; !ok {
t.Errorf("missing required field %q in preference response", f)
}
}
// id, user_id, model_id, provider_config_id should be non-empty strings
for _, f := range []string{"id", "user_id", "model_id", "provider_config_id"} {
v, _ := pref[f].(string)
if v == "" {
t.Errorf("field %q should be a non-empty string, got %v", f, pref[f])
}
}
}
// ── Different provider_config_id = different entry ──
func TestModelPrefs_SameModel_DifferentProvider(t *testing.T) {
h := setupModelPrefsHarness(t)
// Seed a second provider config
config2ID := uuid.New().String()
database.TestDB.Exec(dialectSQL(
"INSERT INTO provider_configs (id, scope, name, provider, endpoint, key_scope) VALUES ($1, 'global', 'Test Provider 2', 'anthropic', 'https://api.anthropic.com/v1', 'global')"),
config2ID)
// Set preference for same model under two providers
h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "dual-provider-model",
"provider_config_id": h.configID,
"hidden": true,
})
h.request("PUT", "/api/v1/models/preferences", h.userToken, map[string]interface{}{
"model_id": "dual-provider-model",
"provider_config_id": config2ID,
"hidden": false,
})
// Should have two distinct entries
resp := h.request("GET", "/api/v1/models/preferences", h.userToken, nil)
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
arr := body["data"].([]interface{})
matches := 0
for _, raw := range arr {
p := raw.(map[string]interface{})
if p["model_id"] == "dual-provider-model" {
matches++
}
}
if matches != 2 {
t.Fatalf("same model, different providers: expected 2 entries, got %d", matches)
}
}