// Package apitest regroupe les tests unitaires et de sécurité de l'API Omnex // (tests en boîte noire, isolés du code de production dans un dossier dédié). package apitest import ( "bytes" "context" "encoding/json" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/omnex/control-plane/api/internal/auth" "github.com/omnex/control-plane/api/internal/config" "github.com/omnex/control-plane/api/internal/demos" "github.com/omnex/control-plane/api/internal/leads" "github.com/omnex/control-plane/api/internal/router" "github.com/omnex/control-plane/api/internal/session" ) const testSecret = "01234567890123456789012345678901" // seedUsers : UserStore mémoire pour les tests. type seedUsers map[string]auth.User func (m seedUsers) ByUsername(username string) (auth.User, bool) { u, ok := m[username]; return u, ok } func (m seedUsers) Create(username, passwordHash string, role auth.Role) (auth.User, error) { u := auth.User{ID: "u-" + username, Username: username, PasswordHash: passwordHash, Role: role} m[username] = u return u, nil } // testEnv : dépendances assemblées pour un test HTTP. type testEnv struct { engine *gin.Engine issuer *auth.Issuer sessions session.Manager } func newTestEnv(t *testing.T) testEnv { return newTestEnvOpts(t) } // newTestEnvOpts permet de configurer un code d'inscription. func newTestEnvOpts(t *testing.T) testEnv { t.Helper() gin.SetMode(gin.TestMode) iss := auth.NewIssuer([]byte(testSecret), time.Minute) sessions := session.NewMemManager(time.Hour) hash, _ := auth.HashPassword("correct-horse") users := seedUsers{"sales": {ID: "u1", Username: "sales", PasswordHash: hash, Role: auth.RoleClient}} demoSvc := demos.NewService(demos.NewMemStore(), demos.NewMemPool(), demos.NoopProvisioner{}, demos.Config{}) deps := router.Deps{ Cfg: config.Config{Env: "test", AllowedOrigins: []string{"http://localhost:5173"}}, Issuer: iss, Sessions: sessions, AuthH: auth.NewHandler(users, sessions, iss, false), LeadsH: leads.NewHandler(leads.NewMemStore()), DemosH: demos.NewHandler(demoSvc), } return testEnv{engine: router.New(deps), issuer: iss, sessions: sessions} } // token : JWT commercial (rôle sales) adossé à une vraie session Redis(mem). func (e testEnv) token(t *testing.T) string { return e.tokenAs(t, auth.RoleClient) } // adminToken : JWT administrateur. func (e testEnv) adminToken(t *testing.T) string { return e.tokenAs(t, auth.RoleAdmin) } func (e testEnv) tokenAs(t *testing.T, role auth.Role) string { t.Helper() sid, err := e.sessions.Create(context.Background(), session.Session{UserID: "u1", Role: string(role)}) if err != nil { t.Fatalf("create session: %v", err) } tok, err := e.issuer.Issue("u1", role, sid) if err != nil { t.Fatalf("issue token: %v", err) } return tok } func (e testEnv) do(method, path, token string, body any) *httptest.ResponseRecorder { var buf bytes.Buffer if body != nil { _ = json.NewEncoder(&buf).Encode(body) } req := httptest.NewRequest(method, path, &buf) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() e.engine.ServeHTTP(w, req) return w } func decode[T any](t *testing.T, w *httptest.ResponseRecorder) T { t.Helper() var v T if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil { t.Fatalf("decode: %v — body=%s", err, w.Body) } return v }