255 lines
9.3 KiB
Go
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)
|
|
}
|
|
}
|