chore: build
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

This commit is contained in:
Xor290
2026-08-06 12:06:05 +02:00
parent c034088bee
commit 22a8d5026c
174 changed files with 30315 additions and 16120 deletions
+405
View File
@@ -0,0 +1,405 @@
package tests
import (
"bytes"
"gestion/handlers"
"net/http"
"net/http/httptest"
"strconv"
"sync"
"testing"
"github.com/gin-gonic/gin"
)
func clientAmendeAndCount(t *testing.T, username string) (amende float64, count int) {
t.Helper()
var row struct {
Amende float64 `gorm:"column:amende"`
CancellationsCount int `gorm:"column:cancellations_count"`
}
if err := testDB.GDB.Raw(
`SELECT COALESCE(amende, 0) AS amende, COALESCE(cancellations_count, 0) AS cancellations_count FROM clients WHERE username = ?`,
username,
).Scan(&row).Error; err != nil {
t.Fatalf("lecture amende/cancellations_count: %v", err)
}
return row.Amende, row.CancellationsCount
}
// ── CalculateCancellationPenalty : barème par défaut ────────────────────────
// (0->20, 1->50, 2->100, 3 et plus->150 — voir DefaultSettings)
func TestCalculateCancellationPenalty_MatchesDefaultTiers(t *testing.T) {
cases := []struct {
cancellationsCount int
wantPenalty int
}{
{0, 20},
{1, 50},
{2, 100},
{3, 150},
{10, 150}, // palier "4e et plus", plafonné
}
for _, c := range cases {
cleanupStockTestData(t)
username := newTestClient(t, "penalty_tier")
testDB.GDB.Exec(`UPDATE clients SET cancellations_count = ? WHERE username = ?`, c.cancellationsCount, username)
got, err := testDB.CalculateCancellationPenalty(username)
if err != nil {
t.Fatalf("CalculateCancellationPenalty (count=%d): %v", c.cancellationsCount, err)
}
if got != c.wantPenalty {
t.Errorf("count=%d: penalty=%d want=%d", c.cancellationsCount, got, c.wantPenalty)
}
}
}
// ── ApplyCancellationPenalty ("client absent" livreur) : cumul + concurrence ─
func TestApplyCancellationPenalty_AccumulatesAcrossSequentialCalls(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "penalty_sequential")
wantPenalties := []int{20, 50, 100, 150}
wantCumulative := []float64{20, 70, 170, 320}
for i, want := range wantPenalties {
penalty, err := testDB.ApplyCancellationPenalty(username)
if err != nil {
t.Fatalf("appel %d: %v", i+1, err)
}
if penalty != want {
t.Errorf("appel %d: penalty=%d want=%d", i+1, penalty, want)
}
amende, count := clientAmendeAndCount(t, username)
if amende != wantCumulative[i] {
t.Errorf("appel %d: amende cumulée=%.2f want=%.2f", i+1, amende, wantCumulative[i])
}
if count != i+1 {
t.Errorf("appel %d: cancellations_count=%d want=%d", i+1, count, i+1)
}
}
}
// Plusieurs "client absent" quasi simultanés sur le même client (deux
// livreurs différents annulant chacun une commande de ce client au même
// moment) ne doivent pas se marcher dessus (verrou FOR UPDATE).
func TestApplyCancellationPenalty_ConcurrentCallsDoNotLoseUpdates(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "penalty_concurrent")
n := 4
var wg sync.WaitGroup
errs := make([]error, n)
for i := range n {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_, errs[idx] = testDB.ApplyCancellationPenalty(username)
}(i)
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Errorf("appel concurrent %d: erreur inattendue: %v", i, err)
}
}
amende, count := clientAmendeAndCount(t, username)
if count != n {
t.Errorf("cancellations_count après %d appels concurrents: got=%d want=%d", n, count, n)
}
if amende != 320 { // 20+50+100+150, un seul palier consommé par appel
t.Errorf("amende après %d appels concurrents: got=%.2f want=320.00 (pas de mise à jour perdue)", n, amende)
}
}
// ── CheckCommandETAExistsAndValid (bug corrigé : lisait la clé avec Get au lieu de HGetAll) ─
func TestCheckCommandETAExistsAndValid_FalseWhenNoETA(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "eta_check_none")
productID := newTestProduct(t, "EtaCheckNone", 5)
cmdID := newTestCommandWithItem(t, username, "assigned", testUserPrefix+"livreurCheckETA", productID, 1, 10)
if testDB.CheckCommandETAExistsAndValid(cmdID) {
t.Error("aucune ETA définie: attendu false")
}
}
func TestCheckCommandETAExistsAndValid_TrueWhenETASet(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "eta_check_valid")
productID := newTestProduct(t, "EtaCheckValid", 5)
cmdID := newTestCommandWithItem(t, username, "assigned", testUserPrefix+"livreurCheckETA2", productID, 1, 10)
if err := testDB.SetCommandETA(cmdID, 15); err != nil {
t.Fatalf("SetCommandETA: %v", err)
}
if !testDB.CheckCommandETAExistsAndValid(cmdID) {
t.Error("ETA valide définie: attendu true")
}
}
// ── CancelCommandAtomic : détection d'annulation tardive et pénalité ────────
func TestCancelCommandAtomic_NoPenaltyWithoutLivreur(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_no_livreur")
productID := newTestProduct(t, "CancelPenaltyNoLivreur", 5)
cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10)
penalty, err := testDB.CancelCommandAtomic(cmdID, username, "test", false)
if err != nil {
t.Fatalf("CancelCommandAtomic: %v", err)
}
if penalty != 0 {
t.Errorf("aucune pénalité attendue sans livreur assigné: got=%d", penalty)
}
amende, count := clientAmendeAndCount(t, username)
if amende != 0 {
t.Errorf("amende doit rester à 0: got=%.2f", amende)
}
if count != 1 {
t.Errorf("cancellations_count doit tout de même être incrémenté: got=%d want=1", count)
}
}
func TestCancelCommandAtomic_LateCancel_EnRoute_RequiresForceConfirmation(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_enroute_noforce")
productID := newTestProduct(t, "CancelPenaltyEnrouteNoforce", 5)
cmdID := newTestCommandWithItem(t, username, "en_route", testUserPrefix+"livreurLate1", productID, 1, 10)
_, err := testDB.CancelCommandAtomic(cmdID, username, "test", false)
if err == nil || err.Error() != "confirmation requise" {
t.Fatalf(`attendu l'erreur "confirmation requise", got=%v`, err)
}
if got := commandStatus(t, cmdID); got != "en_route" {
t.Errorf("le statut ne doit pas changer sans confirmation: got=%s want=en_route", got)
}
if got := productStock(t, productID); got != 5 {
t.Errorf("le stock ne doit pas être remboursé sans confirmation: got=%.2f want=5", got)
}
amende, _ := clientAmendeAndCount(t, username)
if amende != 0 {
t.Errorf("aucune amende ne doit être appliquée sans confirmation: got=%.2f", amende)
}
}
func TestCancelCommandAtomic_LateCancel_Arrived_RequiresForceConfirmation(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_arrived_noforce")
productID := newTestProduct(t, "CancelPenaltyArrivedNoforce", 5)
cmdID := newTestCommandWithItem(t, username, "arrived", testUserPrefix+"livreurLate2", productID, 1, 10)
_, err := testDB.CancelCommandAtomic(cmdID, username, "test", false)
if err == nil || err.Error() != "confirmation requise" {
t.Fatalf(`attendu l'erreur "confirmation requise" pour status=arrived, got=%v`, err)
}
}
func TestCancelCommandAtomic_LateCancelWithForce_AppliesPenalty(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_enroute_force")
productID := newTestProduct(t, "CancelPenaltyEnrouteForce", 5)
cmdID := newTestCommandWithItem(t, username, "en_route", testUserPrefix+"livreurLate3", productID, 2, 20)
penalty, err := testDB.CancelCommandAtomic(cmdID, username, "test", true)
if err != nil {
t.Fatalf("CancelCommandAtomic avec force: %v", err)
}
if penalty != 20 { // 1ère annulation de ce client
t.Errorf("penalty: got=%d want=20", penalty)
}
if got := commandStatus(t, cmdID); got != "cancelled" {
t.Errorf("statut: got=%s want=cancelled", got)
}
if got := productStock(t, productID); got != 7 {
t.Errorf("stock après annulation confirmée (5 initial + 2 remboursés): got=%.2f want=7", got)
}
amende, count := clientAmendeAndCount(t, username)
if amende != 20 {
t.Errorf("amende: got=%.2f want=20", amende)
}
if count != 1 {
t.Errorf("cancellations_count: got=%d want=1", count)
}
}
// Un livreur assigné mais sans statut avancé (assigned) et sans ETA connue
// n'est pas considéré comme une annulation tardive : pas besoin de force.
func TestCancelCommandAtomic_AssignedWithoutETA_NoForceNeeded(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_assigned_noeta")
productID := newTestProduct(t, "CancelPenaltyAssignedNoeta", 5)
cmdID := newTestCommandWithItem(t, username, "assigned", testUserPrefix+"livreurLate4", productID, 1, 10)
penalty, err := testDB.CancelCommandAtomic(cmdID, username, "test", false)
if err != nil {
t.Fatalf("CancelCommandAtomic: %v", err)
}
if penalty != 0 {
t.Errorf("aucune pénalité attendue (pas d'ETA, statut non avancé): got=%d", penalty)
}
}
// Un livreur assigné avec une ETA valide en cache doit être traité comme une
// annulation tardive, même si le statut est encore "assigned" (régression du
// bug CheckCommandETAExistsAndValid corrigé ci-dessus).
func TestCancelCommandAtomic_AssignedWithValidETA_RequiresForce(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_assigned_eta")
productID := newTestProduct(t, "CancelPenaltyAssignedEta", 5)
cmdID := newTestCommandWithItem(t, username, "assigned", testUserPrefix+"livreurLate5", productID, 1, 10)
if err := testDB.SetCommandETA(cmdID, 12); err != nil {
t.Fatalf("SetCommandETA: %v", err)
}
_, err := testDB.CancelCommandAtomic(cmdID, username, "test", false)
if err == nil || err.Error() != "confirmation requise" {
t.Fatalf(`attendu "confirmation requise" (ETA valide définie): got=%v`, err)
}
// Avec force=true, la pénalité doit maintenant s'appliquer.
penalty, err := testDB.CancelCommandAtomic(cmdID, username, "test", true)
if err != nil {
t.Fatalf("CancelCommandAtomic avec force: %v", err)
}
if penalty != 20 {
t.Errorf("penalty: got=%d want=20", penalty)
}
}
func TestCancelCommandAtomic_PenaltyProgressesAcrossMultipleLateCancellations(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_progression")
productID := newTestProduct(t, "CancelPenaltyProgression", 20)
wantPenalties := []int{20, 50, 100, 150}
for i, want := range wantPenalties {
cmdID := newTestCommandWithItem(t, username, "en_route", testUserPrefix+"livreurProg", productID, 1, 10)
penalty, err := testDB.CancelCommandAtomic(cmdID, username, "test", true)
if err != nil {
t.Fatalf("annulation %d: %v", i+1, err)
}
if penalty != want {
t.Errorf("annulation %d: penalty=%d want=%d", i+1, penalty, want)
}
}
amende, count := clientAmendeAndCount(t, username)
if count != 4 {
t.Errorf("cancellations_count: got=%d want=4", count)
}
if amende != 320 { // 20+50+100+150
t.Errorf("amende cumulée: got=%.2f want=320.00", amende)
}
}
func TestCancelCommandAtomic_NonCancellableStatusRejected(t *testing.T) {
for _, status := range []string{"livre", "approved", "cancelled", "disabled"} {
t.Run(status, func(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "cancel_penalty_terminal_"+status)
productID := newTestProduct(t, "CancelPenaltyTerminal"+status, 5)
cmdID := newTestCommandWithItem(t, username, status, "", productID, 1, 10)
if _, err := testDB.CancelCommandAtomic(cmdID, username, "test", true); err == nil {
t.Errorf("statut %q devrait être rejeté même avec force=true", status)
}
})
}
}
func TestCancelCommandAtomic_WrongOwnerRejected(t *testing.T) {
cleanupStockTestData(t)
owner := newTestClient(t, "cancel_penalty_owner")
intruder := newTestClient(t, "cancel_penalty_intruder")
productID := newTestProduct(t, "CancelPenaltyOwner", 5)
cmdID := newTestCommandWithItem(t, owner, "pending", "", productID, 1, 10)
if _, err := testDB.CancelCommandAtomic(cmdID, intruder, "test", false); err == nil {
t.Error("un client ne doit pas pouvoir annuler la commande d'un autre client")
}
}
// ── Flux "client absent" (livreur annule depuis 'arrived') via le handler ───
func deliveryStatusContext(livreurUsername string, commandID int, status, notes string) (*gin.Context, *httptest.ResponseRecorder) {
body := []byte(`{"status":"` + status + `","notes":"` + notes + `"}`)
req := httptest.NewRequest(http.MethodPut, "/x", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = req
c.Set("database", testDB)
c.Set("username", livreurUsername)
c.Set("role", "livreur")
c.Params = gin.Params{{Key: "id", Value: strconv.Itoa(commandID)}}
return c, rec
}
// Quand le livreur marque le client absent (annulation depuis "arrived"),
// l'amende doit être appliquée au CLIENT, jamais au livreur — règle métier
// explicite (comprehension-metier).
func TestUpdateDeliveryStatus_ClientAbsent_AppliesPenaltyToClientNotLivreur(t *testing.T) {
cleanupStockTestData(t)
clientUsername := newTestClient(t, "penalty_absent_client")
livreurUsername := testUserPrefix + "penalty_absent_livreur"
productID := newTestProduct(t, "PenaltyAbsent", 5)
cmdID := newTestCommandWithItem(t, clientUsername, "arrived", livreurUsername, productID, 1, 10)
c, rec := deliveryStatusContext(livreurUsername, cmdID, "cancelled", "Client absent")
handlers.UpdateDeliveryStatus(c)
if rec.Code != http.StatusOK {
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
}
clientAmende, clientCount := clientAmendeAndCount(t, clientUsername)
if clientAmende != 20 {
t.Errorf("amende client après 'client absent': got=%.2f want=20", clientAmende)
}
if clientCount != 1 {
t.Errorf("cancellations_count client: got=%d want=1", clientCount)
}
if got := commandStatus(t, cmdID); got != "cancelled" {
t.Errorf("statut commande: got=%s want=cancelled", got)
}
if got := productStock(t, productID); got != 6 {
t.Errorf("stock après remboursement (5 initial + 1 remboursé): got=%.2f want=6", got)
}
}
// Annuler depuis un statut autre que "arrived"/"livre" (ex: "en_route") ne
// doit PAS déclencher la pénalité "client absent" — ce n'est pas le même
// motif d'annulation.
func TestUpdateDeliveryStatus_CancelFromEnRoute_DoesNotApplyClientAbsentPenalty(t *testing.T) {
cleanupStockTestData(t)
clientUsername := newTestClient(t, "penalty_enroute_client")
livreurUsername := testUserPrefix + "penalty_enroute_livreur"
productID := newTestProduct(t, "PenaltyEnrouteCancel", 5)
cmdID := newTestCommandWithItem(t, clientUsername, "en_route", livreurUsername, productID, 1, 10)
c, rec := deliveryStatusContext(livreurUsername, cmdID, "cancelled", "Problème livraison")
handlers.UpdateDeliveryStatus(c)
if rec.Code != http.StatusOK {
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
}
clientAmende, _ := clientAmendeAndCount(t, clientUsername)
if clientAmende != 0 {
t.Errorf("aucune amende ne doit être appliquée depuis en_route: got=%.2f", clientAmende)
}
}