package apitest import ( "bytes" "net/http" "testing" "github.com/gin-gonic/gin" ) func TestLoginSuccess(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/login", "", gin.H{"username": "sales", "password": "correct-horse"}) if w.Code != http.StatusOK { t.Fatalf("login attendu 200, reçu %d: %s", w.Code, w.Body) } if tok := decode[map[string]any](t, w)["token"]; tok == nil || tok == "" { t.Fatal("login doit renvoyer un token") } // Le cookie de session httpOnly doit être posé. if len(w.Result().Cookies()) == 0 { t.Fatal("login doit poser un cookie de session") } } // Inscription : crée un compte et connecte directement (201 + token). func TestRegisterCreatesAccountAndSession(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/register", "", gin.H{"username": "newsales", "password": "s3cure-pass-1"}) if w.Code != http.StatusCreated { t.Fatalf("register attendu 201, reçu %d: %s", w.Code, w.Body) } tok, _ := decode[map[string]any](t, w)["token"].(string) if tok == "" { t.Fatal("register doit renvoyer un token") } // Le token doit donner accès aux routes protégées. if g := e.do(http.MethodGet, "/api/v1/leads", tok, nil); g.Code != http.StatusOK { t.Fatalf("accès après inscription attendu 200, reçu %d", g.Code) } } // Inscription : username déjà pris => 409. func TestRegisterDuplicateUsername(t *testing.T) { e := newTestEnv(t) // "sales" existe déjà (seedé). w := e.do(http.MethodPost, "/api/v1/auth/register", "", gin.H{"username": "sales", "password": "s3cure-pass-1"}) if w.Code != http.StatusConflict { t.Fatalf("attendu 409, reçu %d", w.Code) } } // Sécurité : mot de passe trop court rejeté à la validation. func TestRegisterRejectsWeakPassword(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/register", "", gin.H{"username": "weakling", "password": "short"}) if w.Code != http.StatusBadRequest { t.Fatalf("attendu 400, reçu %d", w.Code) } } // Sécurité : après logout, le JWT (même valide) est refusé — session Redis révoquée. func TestLogoutRevokesSession(t *testing.T) { e := newTestEnv(t) login := e.do(http.MethodPost, "/api/v1/auth/login", "", gin.H{"username": "sales", "password": "correct-horse"}) tok, _ := decode[map[string]any](t, login)["token"].(string) if w := e.do(http.MethodGet, "/api/v1/leads", tok, nil); w.Code != http.StatusOK { t.Fatalf("accès avant logout attendu 200, reçu %d", w.Code) } if w := e.do(http.MethodPost, "/api/v1/auth/logout", tok, nil); w.Code != http.StatusOK { t.Fatalf("logout attendu 200, reçu %d", w.Code) } // Même token, mais session supprimée => 401. if w := e.do(http.MethodGet, "/api/v1/leads", tok, nil); w.Code != http.StatusUnauthorized { t.Fatalf("après logout attendu 401, reçu %d", w.Code) } } func TestLoginWrongPassword(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/login", "", gin.H{"username": "sales", "password": "wrong-password"}) if w.Code != http.StatusUnauthorized { t.Fatalf("attendu 401, reçu %d", w.Code) } } // Sécurité : pas d'énumération — même réponse pour un utilisateur inconnu. func TestLoginUnknownUserSameResponse(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/login", "", gin.H{"username": "ghost", "password": "whatever8"}) if w.Code != http.StatusUnauthorized { t.Fatalf("attendu 401 (pas d'énumération), reçu %d", w.Code) } } // Sécurité : un username avec caractères spéciaux (tentative d'injection) rejeté à la validation. func TestLoginRejectsNonAlnumUsername(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodPost, "/api/v1/auth/login", "", gin.H{"username": "sales' OR '1'='1", "password": "whatever8"}) if w.Code != http.StatusBadRequest { t.Fatalf("attendu 400 (validation), reçu %d", w.Code) } } func TestLeadsListRequiresAuth(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodGet, "/api/v1/leads", "", nil) if w.Code != http.StatusUnauthorized { t.Fatalf("attendu 401 sans token, reçu %d", w.Code) } } func TestLeadsListRejectsForgedToken(t *testing.T) { e := newTestEnv(t) w := e.do(http.MethodGet, "/api/v1/leads", "eyJ.forged.token", nil) if w.Code != http.StatusUnauthorized { t.Fatalf("attendu 401 token forgé, reçu %d", w.Code) } } // Feature lead : création publique + XSS stocké neutralisé. func TestCreateLeadSanitizesXSS(t *testing.T) { e := newTestEnv(t) payload := gin.H{"company": "", "message": "hi"} w := e.do(http.MethodPost, "/api/v1/leads", "", payload) if w.Code != http.StatusCreated { t.Fatalf("attendu 201, reçu %d: %s", w.Code, w.Body) } list := e.do(http.MethodGet, "/api/v1/leads", e.token(t), nil) if bytes.Contains(list.Body.Bytes(), []byte("