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

161 lines
5.9 KiB
Go

package tests
import (
"sync"
"testing"
)
// SetClientParrainAndCredit lie un parrain à un client ET crédite le parrain
// dans une seule transaction — la doc métier avertit explicitement que sans
// cette atomicité, un crédit peut être appliqué sans lien enregistré, ou
// l'inverse (lien enregistré sans jamais créditer le parrain).
func TestSetClientParrainAndCredit_LinksAndCreditsAtomically(t *testing.T) {
cleanupStockTestData(t)
client := newTestClient(t, "parrain_ok_client")
parrain := newTestClient(t, "parrain_ok_parrain")
setClientReferralBalance(t, parrain, 5)
if err := testDB.SetClientParrainAndCredit(client, parrain, 10); err != nil {
t.Fatalf("SetClientParrainAndCredit: %v", err)
}
got, err := testDB.GetClientParrain(client)
if err != nil {
t.Fatalf("GetClientParrain: %v", err)
}
if got != parrain {
t.Errorf("parrain enregistré: got=%q want=%q", got, parrain)
}
if bal := referralBalance(t, parrain); bal != 15 {
t.Errorf("solde du parrain après crédit (5 + 10): got=%.2f want=15", bal)
}
}
// Un client qui a déjà un parrain ne doit jamais pouvoir en changer via cette
// fonction (contrainte "parrain déjà défini") — et surtout, le nouveau
// parrain proposé ne doit recevoir aucun crédit si le lien est refusé.
func TestSetClientParrainAndCredit_RejectsIfClientAlreadyHasParrain(t *testing.T) {
cleanupStockTestData(t)
client := newTestClient(t, "parrain_already_client")
firstParrain := newTestClient(t, "parrain_already_first")
secondParrain := newTestClient(t, "parrain_already_second")
if err := testDB.SetClientParrainAndCredit(client, firstParrain, 10); err != nil {
t.Fatalf("1er lien: %v", err)
}
if err := testDB.SetClientParrainAndCredit(client, secondParrain, 10); err == nil {
t.Fatal("attendu un rejet : le client a déjà un parrain")
}
if got, _ := testDB.GetClientParrain(client); got != firstParrain {
t.Errorf("le parrain enregistré ne doit pas changer: got=%q want=%q", got, firstParrain)
}
if bal := referralBalance(t, secondParrain); bal != 0 {
t.Errorf("le second parrain (lien refusé) ne doit recevoir aucun crédit: got=%.2f want=0", bal)
}
if bal := referralBalance(t, firstParrain); bal != 10 {
t.Errorf("le premier parrain garde son crédit initial, pas de second crédit: got=%.2f want=10", bal)
}
}
// Scénario exact mis en garde par la doc métier : si le parrain indiqué
// n'existe pas, le crédit échoue — et le lien parrain (première moitié de la
// transaction) doit être annulé avec, pas laissé enregistré tout seul.
func TestSetClientParrainAndCredit_RejectsUnknownParrain_RollsBackLink(t *testing.T) {
cleanupStockTestData(t)
client := newTestClient(t, "parrain_unknown_client")
unknownParrain := testUserPrefix + "does_not_exist_parrain"
if err := testDB.SetClientParrainAndCredit(client, unknownParrain, 10); err == nil {
t.Fatal("attendu une erreur : le parrain n'existe pas")
}
got, err := testDB.GetClientParrain(client)
if err != nil {
t.Fatalf("GetClientParrain: %v", err)
}
if got != "" {
t.Errorf("le lien parrain ne doit PAS être enregistré si le crédit échoue (rollback complet): got=%q want=\"\"", got)
}
}
// Quand le parrainage est désactivé (ReferralEnabled=false côté handler), le
// montant crédité vaut 0 — la liaison doit tout de même réussir sans tenter
// de créditer personne.
func TestSetClientParrainAndCredit_ZeroCreditLinksWithoutCrediting(t *testing.T) {
cleanupStockTestData(t)
client := newTestClient(t, "parrain_zero_client")
parrain := newTestClient(t, "parrain_zero_parrain")
if err := testDB.SetClientParrainAndCredit(client, parrain, 0); err != nil {
t.Fatalf("SetClientParrainAndCredit avec montant nul: %v", err)
}
if got, _ := testDB.GetClientParrain(client); got != parrain {
t.Errorf("le lien doit être enregistré même sans crédit: got=%q want=%q", got, parrain)
}
if bal := referralBalance(t, parrain); bal != 0 {
t.Errorf("aucun crédit ne doit être appliqué avec un montant nul: got=%.2f want=0", bal)
}
}
func TestSetClientParrainAndCredit_RejectsUnknownClient(t *testing.T) {
cleanupStockTestData(t)
parrain := newTestClient(t, "parrain_unknown_client_target")
unknownClient := testUserPrefix + "does_not_exist_client"
if err := testDB.SetClientParrainAndCredit(unknownClient, parrain, 10); err == nil {
t.Fatal("attendu une erreur : le client cible n'existe pas")
}
if bal := referralBalance(t, parrain); bal != 0 {
t.Errorf("le parrain ne doit pas être crédité si le client cible est introuvable: got=%.2f want=0", bal)
}
}
// Deux tentatives concurrentes de parrainage sur le MÊME client (deux
// parrains différents) ne doivent en laisser passer qu'une seule — la
// condition "parrain IS NULL OR parrain = ''" de l'UPDATE sérialise
// naturellement les deux tentatives au niveau de la ligne.
func TestSetClientParrainAndCredit_ConcurrentSetOnSameClientOnlyOneSucceeds(t *testing.T) {
cleanupStockTestData(t)
client := newTestClient(t, "parrain_concurrent_client")
parrainA := newTestClient(t, "parrain_concurrent_a")
parrainB := newTestClient(t, "parrain_concurrent_b")
var wg sync.WaitGroup
errs := make([]error, 2)
wg.Add(2)
go func() {
defer wg.Done()
errs[0] = testDB.SetClientParrainAndCredit(client, parrainA, 10)
}()
go func() {
defer wg.Done()
errs[1] = testDB.SetClientParrainAndCredit(client, parrainB, 10)
}()
wg.Wait()
successCount := 0
for _, err := range errs {
if err == nil {
successCount++
}
}
if successCount != 1 {
t.Errorf("une seule tentative concurrente de parrainage doit réussir: got=%d succès", successCount)
}
finalParrain, _ := testDB.GetClientParrain(client)
if finalParrain != parrainA && finalParrain != parrainB {
t.Fatalf("parrain final inattendu: %q", finalParrain)
}
balA := referralBalance(t, parrainA)
balB := referralBalance(t, parrainB)
if (balA == 10) == (balB == 10) {
t.Errorf("exactement un des deux parrains doit être crédité de 10, pas les deux ni aucun: balA=%.2f balB=%.2f", balA, balB)
}
}