188 lines
5.4 KiB
Go
188 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|