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

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)
}
}