package apitest import ( "errors" "net/http" "testing" "time" "github.com/gin-gonic/gin" "github.com/omnex/control-plane/api/internal/demos" ) func newSvc() *demos.Service { return demos.NewService(demos.NewMemStore(), demos.NewMemPool(), demos.NoopProvisioner{}, demos.Config{}) } // Feature : une démo créée est en provisioning, TTL ~30 j, URL et namespace posés. func TestDemoCreate(t *testing.T) { svc := newSvc() d, err := svc.Create("") if err != nil { t.Fatalf("create: %v", err) } if d.Status != demos.StatusProvisioning { t.Fatalf("statut attendu provisioning, reçu %s", d.Status) } if d.Namespace == "" || d.URL == "" { t.Fatalf("namespace/url manquants: %+v", d) } ttl := d.ExpiresAt.Sub(d.CreatedAt) if ttl < demos.TTL-time.Minute || ttl > demos.TTL+time.Minute { t.Fatalf("TTL attendu ~30j, reçu %s", ttl) } } // Feature : borrow/return du pool cohérents. func TestDemoPoolBorrowReturn(t *testing.T) { pool := demos.NewMemPool() svc := demos.NewService(demos.NewMemStore(), pool, demos.NoopProvisioner{}, demos.Config{}) before, _ := pool.FreeCount() if before[demos.ServiceTelegram] != demos.PoolSizePerService { t.Fatalf("pool initial telegram attendu %d, reçu %d", demos.PoolSizePerService, before[demos.ServiceTelegram]) } d, err := svc.Create("") if err != nil { t.Fatalf("create: %v", err) } after, _ := pool.FreeCount() if after[demos.ServiceTelegram] != demos.PoolSizePerService-1 { t.Fatalf("après emprunt telegram attendu %d, reçu %d", demos.PoolSizePerService-1, after[demos.ServiceTelegram]) } if _, err := svc.Delete(d.ID); err != nil { t.Fatalf("delete: %v", err) } back, _ := pool.FreeCount() if back[demos.ServiceTelegram] != demos.PoolSizePerService { t.Fatalf("après restitution telegram attendu %d, reçu %d", demos.PoolSizePerService, back[demos.ServiceTelegram]) } } // Feature : capacité — la 6ᵉ démo est refusée (5 slots de pool). func TestDemoCapacityReached(t *testing.T) { svc := newSvc() for i := 0; i < demos.MaxConcurrentDemos; i++ { if _, err := svc.Create(""); err != nil { t.Fatalf("create %d: %v", i, err) } } if _, err := svc.Create(""); !errors.Is(err, demos.ErrCapacityReached) { t.Fatalf("6ᵉ démo : attendu ErrCapacityReached, reçu %v", err) } } // Feature : la capacité se libère après destruction. func TestDemoCapacityFreesAfterDelete(t *testing.T) { svc := newSvc() var first demos.Demo for i := 0; i < demos.MaxConcurrentDemos; i++ { d, err := svc.Create("") if err != nil { t.Fatalf("create %d: %v", i, err) } if i == 0 { first = d } } if _, err := svc.Delete(first.ID); err != nil { t.Fatalf("delete: %v", err) } if _, err := svc.Create(""); err != nil { t.Fatalf("après libération : create doit réussir, reçu %v", err) } } // Feature : extend prolonge de 30 j. func TestDemoExtend(t *testing.T) { svc := newSvc() d, _ := svc.Create("") ext, err := svc.Extend(d.ID) if err != nil { t.Fatalf("extend: %v", err) } if !ext.ExpiresAt.After(d.ExpiresAt) { t.Fatalf("extend doit repousser l'échéance: avant=%s après=%s", d.ExpiresAt, ext.ExpiresAt) } } // Feature : delete idempotent + not found. func TestDemoDeleteNotFound(t *testing.T) { svc := newSvc() if _, err := svc.Delete("inconnu"); !errors.Is(err, demos.ErrNotFound) { t.Fatalf("attendu ErrNotFound, reçu %v", err) } } // --- Tests HTTP (routes protégées) --- func TestDemosEndpointRequiresAuth(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/demos", "", gin.H{}) if w.Code != http.StatusUnauthorized { t.Fatalf("attendu 401 sans token, reçu %d", w.Code) } } // Sécurité : un commercial (sales/« client ») ne peut PAS provisionner de démo. func TestDemosForbiddenForSales(t *testing.T) { e := newTestEnv(t) sales := e.token(t) // rôle sales for _, tc := range []struct{ method, path string }{ {http.MethodPost, "/api/v1/demos"}, {http.MethodGet, "/api/v1/demos"}, {http.MethodDelete, "/api/v1/demos/x"}, {http.MethodPost, "/api/v1/demos/x/extend"}, } { w := e.do(tc.method, tc.path, sales, gin.H{}) if w.Code != http.StatusForbidden { t.Fatalf("%s %s : attendu 403 pour un commercial, reçu %d", tc.method, tc.path, w.Code) } } } func TestDemosCreateAndGet(t *testing.T) { e := newTestEnv(t) tok := e.adminToken(t) w := e.do(http.MethodPost, "/api/v1/demos", tok, gin.H{}) if w.Code != http.StatusAccepted { t.Fatalf("attendu 202, reçu %d: %s", w.Code, w.Body) } created := decode[demos.Demo](t, w) g := e.do(http.MethodGet, "/api/v1/demos/"+created.ID, tok, nil) if g.Code != http.StatusOK { t.Fatalf("get attendu 200, reçu %d", g.Code) } if u := decode[demos.Demo](t, g); u.ID != created.ID { t.Fatalf("id incohérent: %s != %s", u.ID, created.ID) } } func TestDemosGetUnknown404(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodGet, "/api/v1/demos/inconnu", e.adminToken(t), nil) if w.Code != http.StatusNotFound { t.Fatalf("attendu 404, reçu %d", w.Code) } } // Capacité via HTTP : la 6ᵉ création renvoie 409. func TestDemosCapacityHTTP(t *testing.T) { e := newTestEnv(t) tok := e.adminToken(t) for i := 0; i < demos.MaxConcurrentDemos; i++ { if w := e.do(http.MethodPost, "/api/v1/demos", tok, gin.H{}); w.Code != http.StatusAccepted { t.Fatalf("create %d attendu 202, reçu %d", i, w.Code) } } w := e.do(http.MethodPost, "/api/v1/demos", tok, gin.H{}) if w.Code != http.StatusConflict { t.Fatalf("6ᵉ démo attendu 409, reçu %d", w.Code) } }