package service import ( "encoding/json" "strconv" "testing" "github.com/alicebob/miniredis/v2" "github.com/ez-api/ez-api/internal/model" "github.com/redis/go-redis/v9" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type bindingSnapshot struct { Namespace string `json:"namespace"` PublicModel string `json:"public_model"` RouteGroup string `json:"route_group"` Upstreams map[string]string `json:"upstreams"` } func TestSyncBindings_SelectorExact(t *testing.T) { t.Parallel() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.Provider{}, &model.Binding{}); err != nil { t.Fatalf("migrate: %v", err) } p := model.Provider{Name: "p1", Type: "openai", Group: "rg", Models: "m"} if err := db.Create(&p).Error; err != nil { t.Fatalf("create provider: %v", err) } b := model.Binding{Namespace: "ns", PublicModel: "m", RouteGroup: "rg", SelectorType: "exact", Status: "active"} if err := db.Create(&b).Error; err != nil { t.Fatalf("create binding: %v", err) } mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) svc := NewSyncService(rdb) if err := svc.SyncBindings(db); err != nil { t.Fatalf("SyncBindings: %v", err) } raw := mr.HGet("config:bindings", "ns.m") if raw == "" { t.Fatalf("expected config:bindings entry") } var snap bindingSnapshot if err := json.Unmarshal([]byte(raw), &snap); err != nil { t.Fatalf("unmarshal: %v", err) } if snap.Upstreams == nil || snap.Upstreams[jsonID(p.ID)] != "m" { t.Fatalf("unexpected upstreams: %+v", snap.Upstreams) } } func TestSyncBindings_SelectorRegexAndNormalize(t *testing.T) { t.Parallel() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.Provider{}, &model.Binding{}); err != nil { t.Fatalf("migrate: %v", err) } p1 := model.Provider{Name: "p1", Type: "openai", Group: "rg", Models: "moonshot/kimi2,kimi2"} p2 := model.Provider{Name: "p2", Type: "openai", Group: "rg", Models: "moonshot/kimi2"} if err := db.Create(&p1).Error; err != nil { t.Fatalf("create provider1: %v", err) } if err := db.Create(&p2).Error; err != nil { t.Fatalf("create provider2: %v", err) } // Regex should match uniquely (moonshot/kimi2 only). bRegex := model.Binding{Namespace: "ns", PublicModel: "kimi2", RouteGroup: "rg", SelectorType: "regex", SelectorValue: "^moonshot/kimi2$", Status: "active"} if err := db.Create(&bRegex).Error; err != nil { t.Fatalf("create binding regex: %v", err) } // Normalize_exact should match p2 (moonshot/kimi2) for "kimi2". bNorm := model.Binding{Namespace: "ns", PublicModel: "kimi2-n", RouteGroup: "rg", SelectorType: "normalize_exact", SelectorValue: "kimi2", Status: "active"} if err := db.Create(&bNorm).Error; err != nil { t.Fatalf("create binding normalize: %v", err) } mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) svc := NewSyncService(rdb) if err := svc.SyncBindings(db); err != nil { t.Fatalf("SyncBindings: %v", err) } // Regex binding should include both providers mapped to moonshot/kimi2 (p1 also has it). raw := mr.HGet("config:bindings", "ns.kimi2") var snapRegex bindingSnapshot if err := json.Unmarshal([]byte(raw), &snapRegex); err != nil { t.Fatalf("unmarshal regex: %v", err) } if snapRegex.Upstreams[jsonID(p1.ID)] != "moonshot/kimi2" || snapRegex.Upstreams[jsonID(p2.ID)] != "moonshot/kimi2" { t.Fatalf("unexpected regex upstreams: %+v", snapRegex.Upstreams) } // Normalize_exact binding should include p2 but exclude p1 due to multi-match (moonshot/kimi2 + kimi2). raw = mr.HGet("config:bindings", "ns.kimi2-n") var snapNorm bindingSnapshot if err := json.Unmarshal([]byte(raw), &snapNorm); err != nil { t.Fatalf("unmarshal normalize: %v", err) } if snapNorm.Upstreams[jsonID(p2.ID)] != "moonshot/kimi2" { t.Fatalf("expected p2 upstream, got %+v", snapNorm.Upstreams) } if _, ok := snapNorm.Upstreams[jsonID(p1.ID)]; ok { t.Fatalf("did not expect p1 upstream due to normalize multi-match, got %+v", snapNorm.Upstreams) } } func jsonID(id uint) string { return strconv.FormatUint(uint64(id), 10) }