package api import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/alicebob/miniredis/v2" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newTestHandlerWithRedis(t *testing.T) (*Handler, *gorm.DB, *miniredis.Miniredis) { t.Helper() gin.SetMode(gin.TestMode) dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.Provider{}, &model.Binding{}, &model.Model{}); err != nil { t.Fatalf("migrate: %v", err) } mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) sync := service.NewSyncService(rdb) return NewHandler(db, db, sync, nil, rdb, nil), db, mr } func TestCreateModel_DefaultsKindChat_AndWritesModelsMeta(t *testing.T) { h, _, mr := newTestHandlerWithRedis(t) r := gin.New() r.POST("/admin/models", h.CreateModel) reqBody := map[string]any{ "name": "ns.m", } b, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) if rr.Code != http.StatusCreated { t.Fatalf("expected 201, got %d body=%s", rr.Code, rr.Body.String()) } raw := mr.HGet("meta:models", "ns.m") if raw == "" { t.Fatalf("expected meta:models[ns.m] to be written") } var snap map[string]any if err := json.Unmarshal([]byte(raw), &snap); err != nil { t.Fatalf("unmarshal snapshot: %v raw=%s", err, raw) } if snap["kind"] != "chat" { t.Fatalf("expected kind=chat, got %v raw=%s", snap["kind"], raw) } if v := mr.HGet("meta:models_meta", "version"); v == "" { t.Fatalf("expected meta:models_meta.version") } if v := mr.HGet("meta:models_meta", "updated_at"); v == "" { t.Fatalf("expected meta:models_meta.updated_at") } if v := mr.HGet("meta:models_meta", "source"); v == "" { t.Fatalf("expected meta:models_meta.source") } if v := mr.HGet("meta:models_meta", "checksum"); v == "" { t.Fatalf("expected meta:models_meta.checksum") } } func TestCreateModel_InvalidKind_Returns400(t *testing.T) { h, _, _ := newTestHandlerWithRedis(t) r := gin.New() r.POST("/admin/models", h.CreateModel) reqBody := map[string]any{ "name": "ns.m2", "kind": "bad", } b, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", rr.Code, rr.Body.String()) } } func TestDeleteModel_RemovesMeta(t *testing.T) { h, db, mr := newTestHandlerWithRedis(t) r := gin.New() r.POST("/admin/models", h.CreateModel) r.DELETE("/admin/models/:id", h.DeleteModel) reqBody := map[string]any{ "name": "ns.del", } b, _ := json.Marshal(reqBody) req := httptest.NewRequest(http.MethodPost, "/admin/models", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() r.ServeHTTP(rr, req) if rr.Code != http.StatusCreated { t.Fatalf("expected 201, got %d body=%s", rr.Code, rr.Body.String()) } var created model.Model if err := json.Unmarshal(rr.Body.Bytes(), &created); err != nil { t.Fatalf("unmarshal: %v", err) } delReq := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/models/%d", created.ID), nil) delRec := httptest.NewRecorder() r.ServeHTTP(delRec, delReq) if delRec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", delRec.Code, delRec.Body.String()) } if raw := mr.HGet("meta:models", "ns.del"); raw != "" { t.Fatalf("expected meta:models[ns.del] removed, got %q", raw) } var remaining int64 if err := db.Model(&model.Model{}).Where("name = ?", "ns.del").Count(&remaining).Error; err != nil { t.Fatalf("count: %v", err) } if remaining != 0 { t.Fatalf("expected model deleted, got count=%d", remaining) } }