Files
Xor290 22a8d5026c
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s
chore: build
2026-08-06 12:06:05 +02:00

255 lines
9.3 KiB
Go

package tests
import (
"sync"
"testing"
)
// setClientReferralBalance fixe directement le solde de crédit parrainage
// d'un client de test (contourne le flux normal de parrainage/checkout pour
// tester isolément le débit/crédit).
func setClientReferralBalance(t *testing.T, username string, amount float64) {
t.Helper()
if err := testDB.GDB.Exec(
`UPDATE clients SET referral_balance = ? WHERE username = ?`, amount, username,
).Error; err != nil {
t.Fatalf("setClientReferralBalance: %v", err)
}
}
func referralBalance(t *testing.T, username string) float64 {
t.Helper()
balance, err := testDB.GetClientReferralBalance(username)
if err != nil {
t.Fatalf("GetClientReferralBalance: %v", err)
}
return balance
}
// ── DebitReferralBalance ─────────────────────────────────────────────────────
func TestDebitReferralBalance_SucceedsWithSufficientBalance(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_debit_ok")
setClientReferralBalance(t, username, 50)
if err := testDB.DebitReferralBalance(username, 30); err != nil {
t.Fatalf("DebitReferralBalance: %v", err)
}
if got := referralBalance(t, username); got != 20 {
t.Errorf("solde après débit (50 - 30): got=%.2f want=20", got)
}
}
func TestDebitReferralBalance_FailsWithInsufficientBalance(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_debit_insuff")
setClientReferralBalance(t, username, 10)
if err := testDB.DebitReferralBalance(username, 30); err == nil {
t.Fatal("attendu une erreur (solde 10€ insuffisant pour débiter 30€)")
}
if got := referralBalance(t, username); got != 10 {
t.Errorf("solde ne doit pas bouger si le débit échoue: got=%.2f want=10", got)
}
}
// Un montant nul ou négatif est un no-op silencieux (cas "pas de crédit
// parrainage utilisé" au checkout) — ne doit jamais faire échouer ni modifier
// le solde.
func TestDebitReferralBalance_ZeroOrNegativeAmountIsNoop(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_debit_zero")
setClientReferralBalance(t, username, 15)
if err := testDB.DebitReferralBalance(username, 0); err != nil {
t.Errorf("montant nul ne doit jamais échouer: %v", err)
}
if err := testDB.DebitReferralBalance(username, -5); err != nil {
t.Errorf("montant négatif ne doit jamais échouer: %v", err)
}
if got := referralBalance(t, username); got != 15 {
t.Errorf("solde ne doit pas bouger sur un débit nul/négatif: got=%.2f want=15", got)
}
}
// Trois débits concurrents pour un solde qui ne permet qu'un seul d'entre eux
// ne doivent en laisser passer qu'un seul (verrou FOR UPDATE) — même classe de
// bug que le double-submit checkout, appliquée au solde de parrainage.
func TestDebitReferralBalance_ConcurrentDebitsDoNotOverspend(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_debit_concurrent")
setClientReferralBalance(t, username, 30)
var wg sync.WaitGroup
n := 3
errs := make([]error, n)
for i := range n {
wg.Add(1)
go func(idx int) {
defer wg.Done()
errs[idx] = testDB.DebitReferralBalance(username, 30)
}(i)
}
wg.Wait()
successCount := 0
for _, err := range errs {
if err == nil {
successCount++
}
}
if successCount != 1 {
t.Errorf("un seul débit concurrent de 30€ sur un solde de 30€ doit réussir: got=%d succès", successCount)
}
if got := referralBalance(t, username); got != 0 {
t.Errorf("solde final après débits concurrents: got=%.2f want=0 (un seul débit appliqué)", got)
}
}
// ── CreditClientReferral ─────────────────────────────────────────────────────
func TestCreditClientReferral_AddsAmount(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_credit_ok")
setClientReferralBalance(t, username, 10)
if err := testDB.CreditClientReferral(username, 25); err != nil {
t.Fatalf("CreditClientReferral: %v", err)
}
if got := referralBalance(t, username); got != 35 {
t.Errorf("solde après crédit (10 + 25): got=%.2f want=35", got)
}
}
func TestCreditClientReferral_RejectsNonPositiveAmount(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_credit_negative")
setClientReferralBalance(t, username, 10)
if err := testDB.CreditClientReferral(username, 0); err == nil {
t.Error("un crédit de montant nul doit être rejeté")
}
if err := testDB.CreditClientReferral(username, -5); err == nil {
t.Error("un crédit de montant négatif doit être rejeté")
}
if got := referralBalance(t, username); got != 10 {
t.Errorf("solde ne doit pas bouger sur un crédit rejeté: got=%.2f want=10", got)
}
}
func TestCreditClientReferral_FailsForUnknownClient(t *testing.T) {
if err := testDB.CreditClientReferral(testUserPrefix+"does_not_exist", 10); err == nil {
t.Fatal("attendu une erreur pour un client inexistant")
}
}
// ── ResetClientReferralBalance ───────────────────────────────────────────────
func TestResetClientReferralBalance_SetsToZero(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_reset")
setClientReferralBalance(t, username, 42)
if err := testDB.ResetClientReferralBalance(username); err != nil {
t.Fatalf("ResetClientReferralBalance: %v", err)
}
if got := referralBalance(t, username); got != 0 {
t.Errorf("solde après reset: got=%.2f want=0", got)
}
}
// ── Séquence complète débit -> checkout, reproduisant exactement le flux du
// handler ValidateBasket (handlers/panier.go) : débit AVANT la création de la
// commande, puis re-crédit compensatoire si CreateCommandWithAddress échoue.
// Sans ce re-crédit, un client perdrait sèchement son crédit de parrainage
// sur un checkout qui a pourtant échoué (violation explicite de la doc métier).
func TestReferral_DebitThenCheckoutFailure_RecreditsBalance(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_checkout_fail")
productID := newTestProduct(t, "ReferralCheckoutFail", 1)
setClientReferralBalance(t, username, 20)
insertNormalBasketRow(t, username, productID, 5, 50) // 5 demandés pour 1 en stock -> échec garanti
referralUsed := 20.0
if err := testDB.DebitReferralBalance(username, referralUsed); err != nil {
t.Fatalf("DebitReferralBalance: %v", err)
}
if got := referralBalance(t, username); got != 0 {
t.Fatalf("précondition: solde débité: got=%.2f want=0", got)
}
_, err := testDB.CreateCommandWithAddress(username, "1 rue de test")
if err == nil {
t.Fatal("attendu un échec de checkout (stock insuffisant)")
}
// Reproduction exacte de la compensation faite par le handler en cas d'échec.
if err := testDB.CreditClientReferral(username, referralUsed); err != nil {
t.Fatalf("CreditClientReferral (compensation): %v", err)
}
if got := referralBalance(t, username); got != 20 {
t.Errorf("le crédit parrainage doit être intégralement restauré après échec du checkout: got=%.2f want=20", got)
}
if got := productStock(t, productID); got != 1 {
t.Errorf("stock ne doit pas bouger si le checkout échoue: got=%.2f want=1", got)
}
}
func TestReferral_DebitThenCheckoutSuccess_BalanceStaysDebited(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_checkout_ok")
productID := newTestProduct(t, "ReferralCheckoutOk", 10)
setClientReferralBalance(t, username, 20)
insertNormalBasketRow(t, username, productID, 3, 30)
if err := testDB.DebitReferralBalance(username, 20); err != nil {
t.Fatalf("DebitReferralBalance: %v", err)
}
if _, err := testDB.CreateCommandWithAddress(username, "1 rue de test"); err != nil {
t.Fatalf("CreateCommandWithAddress: %v", err)
}
if got := referralBalance(t, username); got != 0 {
t.Errorf("le crédit parrainage reste débité après un checkout réussi: got=%.2f want=0", got)
}
if got := productStock(t, productID); got != 7 {
t.Errorf("stock après checkout réussi: got=%.2f want=7", got)
}
}
// Combine les deux règles demandées explicitement : réclamation de
// récompense (même produit en reward + en achat normal) ET utilisation du
// crédit de parrainage sur la même commande.
func TestReferral_CombinedWithSameProductRewardAndNormal_AllInvariantsHold(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "referral_combined_reward")
productID := newTestProduct(t, "ReferralCombinedReward", 20)
setClientReferralBalance(t, username, 15)
insertRewardBasketRow(t, username, productID, 1)
insertNormalBasketRow(t, username, productID, 4, 40)
if err := testDB.DebitReferralBalance(username, 15); err != nil {
t.Fatalf("DebitReferralBalance: %v", err)
}
if _, err := testDB.CreateCommandWithAddress(username, "1 rue de test"); err != nil {
t.Fatalf("CreateCommandWithAddress: %v", err)
}
// 20 initial - 1 (reward) - 4 (normal) = 15.
if got := productStock(t, productID); got != 15 {
t.Errorf("stock après checkout combiné (reward+normal même produit + parrainage): got=%.2f want=15", got)
}
if got := referralBalance(t, username); got != 0 {
t.Errorf("crédit parrainage débité et non restauré après succès: got=%.2f want=0", got)
}
}