This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ── Client (CancelCommandAtomic) ────────────────────────────────────────────
|
||||
|
||||
func TestCancelCommandAtomic_RefundsStockExactly(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_client_ok")
|
||||
productID := newTestProduct(t, "CancelClientOk", 5)
|
||||
// pending, sans livreur assigné -> annulation sans pénalité ni confirmation requise
|
||||
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 3, 30)
|
||||
|
||||
if _, err := testDB.CancelCommandAtomic(cmdID, username, "test", false); err != nil {
|
||||
t.Fatalf("CancelCommandAtomic: %v", err)
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 8 {
|
||||
t.Errorf("stock après annulation client (5 initial + 3 remboursés): got=%.2f want=8", got)
|
||||
}
|
||||
if got := commandStatus(t, cmdID); got != "cancelled" {
|
||||
t.Errorf("statut après annulation: got=%s want=cancelled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelCommandAtomic_DoubleCancelDoesNotDoubleRefund(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_client_dbl")
|
||||
productID := newTestProduct(t, "CancelClientDbl", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 3, 30)
|
||||
|
||||
if _, err := testDB.CancelCommandAtomic(cmdID, username, "test", false); err != nil {
|
||||
t.Fatalf("1er CancelCommandAtomic: %v", err)
|
||||
}
|
||||
// Rejeu (double-tap / retry réseau) : doit échouer proprement, pas de second remboursement.
|
||||
if _, err := testDB.CancelCommandAtomic(cmdID, username, "test", false); err == nil {
|
||||
t.Fatal("le second appel sur une commande déjà annulée doit renvoyer une erreur")
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 8 {
|
||||
t.Errorf("stock après double annulation (doit rester remboursé une seule fois): got=%.2f want=8", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelCommandAtomic_ConcurrentCancelDoesNotDoubleRefund(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_client_concurrent")
|
||||
productID := newTestProduct(t, "CancelClientConcurrent", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 3, 30)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]error, 3)
|
||||
for i := range 3 {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
_, results[idx] = testDB.CancelCommandAtomic(cmdID, username, "test", false)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
successCount := 0
|
||||
for _, err := range results {
|
||||
if err == nil {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
if successCount != 1 {
|
||||
t.Errorf("un seul appel concurrent doit réussir l'annulation: got=%d succès", successCount)
|
||||
}
|
||||
if got := productStock(t, productID); got != 8 {
|
||||
t.Errorf("stock après 3 annulations concurrentes de la même commande: got=%.2f want=8 (un seul remboursement)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Livreur (CancelDeliveryByLivreurAtomic) ─────────────────────────────────
|
||||
|
||||
func TestCancelDeliveryByLivreurAtomic_RefundsStockExactly(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_livreur_ok")
|
||||
productID := newTestProduct(t, "CancelLivreurOk", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "en_route", testUserPrefix+"livreurX", productID, 2, 20)
|
||||
|
||||
alreadyCancelled, prevStatus, err := testDB.CancelDeliveryByLivreurAtomic(cmdID)
|
||||
if err != nil {
|
||||
t.Fatalf("CancelDeliveryByLivreurAtomic: %v", err)
|
||||
}
|
||||
if alreadyCancelled {
|
||||
t.Error("alreadyCancelled ne doit pas être true au premier appel")
|
||||
}
|
||||
if prevStatus != "en_route" {
|
||||
t.Errorf("prevStatus: got=%s want=en_route", prevStatus)
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 7 {
|
||||
t.Errorf("stock après annulation livreur (5 initial + 2 remboursés): got=%.2f want=7", got)
|
||||
}
|
||||
if got := commandStatus(t, cmdID); got != "cancelled" {
|
||||
t.Errorf("statut après annulation livreur: got=%s want=cancelled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelDeliveryByLivreurAtomic_DoubleCancelIsIdempotent(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_livreur_dbl")
|
||||
productID := newTestProduct(t, "CancelLivreurDbl", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "arrived", testUserPrefix+"livreurX", productID, 2, 20)
|
||||
|
||||
if _, _, err := testDB.CancelDeliveryByLivreurAtomic(cmdID); err != nil {
|
||||
t.Fatalf("1er appel: %v", err)
|
||||
}
|
||||
|
||||
alreadyCancelled, _, err := testDB.CancelDeliveryByLivreurAtomic(cmdID)
|
||||
if err != nil {
|
||||
t.Fatalf("2e appel (doit être idempotent, pas une erreur): %v", err)
|
||||
}
|
||||
if !alreadyCancelled {
|
||||
t.Error("le 2e appel doit renvoyer alreadyCancelled=true")
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 7 {
|
||||
t.Errorf("stock après double annulation livreur (doit rester remboursé une seule fois): got=%.2f want=7", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelDeliveryByLivreurAtomic_ConcurrentCancelDoesNotDoubleRefund(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_livreur_concurrent")
|
||||
productID := newTestProduct(t, "CancelLivreurConcurrent", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "en_route", testUserPrefix+"livreurX", productID, 2, 20)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
n := 3
|
||||
alreadyFlags := make([]bool, n)
|
||||
errs := make([]error, n)
|
||||
for i := range n {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
alreadyFlags[idx], _, errs[idx] = testDB.CancelDeliveryByLivreurAtomic(cmdID)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
freshCancelCount := 0
|
||||
for i := range n {
|
||||
if errs[i] != nil {
|
||||
t.Errorf("appel %d: erreur inattendue: %v", i, errs[i])
|
||||
continue
|
||||
}
|
||||
if !alreadyFlags[i] {
|
||||
freshCancelCount++
|
||||
}
|
||||
}
|
||||
if freshCancelCount != 1 {
|
||||
t.Errorf("un seul appel concurrent doit effectuer l'annulation réelle: got=%d", freshCancelCount)
|
||||
}
|
||||
if got := productStock(t, productID); got != 7 {
|
||||
t.Errorf("stock après annulations concurrentes livreur: got=%.2f want=7 (un seul remboursement)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin/Cabine (DeleteCommandAtomic) ──────────────────────────────────────
|
||||
|
||||
func TestDeleteCommandAtomic_RefundsStockAndRemovesCommand(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "delete_admin_ok")
|
||||
productID := newTestProduct(t, "DeleteAdminOk", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 4, 40)
|
||||
|
||||
if err := testDB.DeleteCommandAtomic(cmdID, "admin_test", "admin"); err != nil {
|
||||
t.Fatalf("DeleteCommandAtomic: %v", err)
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 9 {
|
||||
t.Errorf("stock après suppression admin (5 initial + 4 remboursés): got=%.2f want=9", got)
|
||||
}
|
||||
|
||||
var count int64
|
||||
testDB.GDB.Raw(`SELECT COUNT(*) FROM commandes WHERE id = ?`, cmdID).Scan(&count)
|
||||
if count != 0 {
|
||||
t.Errorf("la commande doit être supprimée de la base: got=%d lignes restantes", count)
|
||||
}
|
||||
}
|
||||
|
||||
// Si la commande est déjà annulée ou approuvée, le stock a déjà été traité
|
||||
// par le chemin correspondant — DeleteCommandAtomic ne doit pas rembourser
|
||||
// une seconde fois lors d'une suppression a posteriori.
|
||||
func TestDeleteCommandAtomic_DoesNotRefundAlreadyCancelledOrApprovedOrder(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "delete_admin_already")
|
||||
productID := newTestProduct(t, "DeleteAdminAlready", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "cancelled", "", productID, 4, 40)
|
||||
|
||||
if err := testDB.DeleteCommandAtomic(cmdID, "admin_test", "admin"); err != nil {
|
||||
t.Fatalf("DeleteCommandAtomic: %v", err)
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 5 {
|
||||
t.Errorf("stock ne doit pas être remboursé pour une commande déjà annulée: got=%.2f want=5", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Crypto (CancelCryptoCommand) ────────────────────────────────────────────
|
||||
|
||||
func TestCancelCryptoCommand_RefundsStockOnPendingPayment(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_crypto_ok")
|
||||
productID := newTestProduct(t, "CancelCryptoOk", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending_payment", "", productID, 2, 20)
|
||||
|
||||
if err := testDB.CancelCryptoCommand(cmdID); err != nil {
|
||||
t.Fatalf("CancelCryptoCommand: %v", err)
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 7 {
|
||||
t.Errorf("stock après annulation crypto (5 initial + 2 remboursés): got=%.2f want=7", got)
|
||||
}
|
||||
if got := commandStatus(t, cmdID); got != "cancelled" {
|
||||
t.Errorf("statut après annulation crypto: got=%s want=cancelled", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelCryptoCommand_RejectsIfNotPendingPayment(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_crypto_wrong")
|
||||
productID := newTestProduct(t, "CancelCryptoWrong", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 2, 20)
|
||||
|
||||
if err := testDB.CancelCryptoCommand(cmdID); err == nil {
|
||||
t.Fatal("attendu une erreur : seule une commande pending_payment est annulable via ce chemin")
|
||||
}
|
||||
if got := productStock(t, productID); got != 5 {
|
||||
t.Errorf("stock ne doit pas bouger si le statut n'est pas pending_payment: got=%.2f want=5", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelCryptoCommand_DoubleCancelDoesNotDoubleRefund(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "cancel_crypto_dbl")
|
||||
productID := newTestProduct(t, "CancelCryptoDbl", 5)
|
||||
cmdID := newTestCommandWithItem(t, username, "pending_payment", "", productID, 2, 20)
|
||||
|
||||
if err := testDB.CancelCryptoCommand(cmdID); err != nil {
|
||||
t.Fatalf("1er appel: %v", err)
|
||||
}
|
||||
if err := testDB.CancelCryptoCommand(cmdID); err == nil {
|
||||
t.Fatal("le 2e appel (webhook rejoué) doit échouer, pas rembourser une seconde fois")
|
||||
}
|
||||
|
||||
if got := productStock(t, productID); got != 7 {
|
||||
t.Errorf("stock après webhook rejoué: got=%.2f want=7 (un seul remboursement)", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user