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("