143 lines
5.1 KiB
Go
143 lines
5.1 KiB
Go
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": "<script>alert(1)</script>", "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("<script>")) {
|
|
t.Fatal("le HTML brut ne doit pas être stocké/renvoyé (XSS)")
|
|
}
|
|
}
|
|
|
|
func TestCreateLeadRejectsInvalidEmail(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
w := e.do(http.MethodPost, "/api/v1/leads", "", gin.H{"company": "Corp", "email": "not-an-email"})
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("attendu 400, reçu %d", w.Code)
|
|
}
|
|
}
|