161 lines
5.9 KiB
Go
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)
|
|
}
|
|
}
|