285 lines
11 KiB
Go
285 lines
11 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
// Régression: une commande "livrée" (marchandise déjà sortie du stock au
|
|
// checkout, remise au client) ne doit pas voir son stock remboursé lors
|
|
// d'une suppression — symétrique à CancelCommandByAdminAtomic qui exclut
|
|
// déjà "livre" de son remboursement.
|
|
func TestDeleteCommandAtomic_DoesNotRefundDeliveredOrder(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
username := newTestClient(t, "delete_admin_livre")
|
|
productID := newTestProduct(t, "DeleteAdminLivre", 5)
|
|
cmdID := newTestCommandWithItem(t, username, "livre", "", 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à livrée: got=%.2f want=5", got)
|
|
}
|
|
}
|
|
|
|
func TestDeleteCommandAtomic_UnknownCommandReturnsError(t *testing.T) {
|
|
cleanupStockTestData(t)
|
|
err := testDB.DeleteCommandAtomic(99999999, "admin_test", "admin")
|
|
if err == nil {
|
|
t.Fatal("DeleteCommandAtomic sur une commande inexistante doit retourner une erreur")
|
|
}
|
|
}
|
|
|
|
// ── 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)
|
|
}
|
|
}
|