package tests import ( "encoding/json" "gestion/db" "gestion/models" "testing" "time" ) // newTestProductWithCategory crée un produit de test avec une catégorie // personnalisée (contrairement à newTestProduct qui pose toujours "test") — // nécessaire ici pour distinguer les catégories dans le routage par livreur. func newTestProductWithCategory(t *testing.T, name, category string, stock float64) int { t.Helper() fullName := testProductPrefix + name var id int if err := testDB.GDB.Raw( `INSERT INTO products (name, category, description, stock) VALUES (?, ?, '', ?) RETURNING id`, fullName, category, stock, ).Scan(&id).Error; err != nil { t.Fatalf("création produit test %q: %v", fullName, err) } if err := testDB.GDB.Exec( `INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, 1, 10.00, true)`, id, ).Error; err != nil { t.Fatalf("création prix produit test %q: %v", fullName, err) } t.Cleanup(func() { testDB.GDB.Exec(`DELETE FROM product_prices WHERE product_id = ?`, id) testDB.GDB.Exec(`DELETE FROM products WHERE id = ?`, id) }) return id } // setLivreurStatus place directement en Redis le statut d'un livreur, comme // le ferait l'app livreur en production (clé "delivery:status:{username}"). func setLivreurStatus(t *testing.T, username, status string) { t.Helper() data, err := json.Marshal(models.DeliveryPersonStatus{ Username: username, Status: status, LastUpdate: time.Now(), }) if err != nil { t.Fatalf("marshal DeliveryPersonStatus: %v", err) } key := "delivery:status:" + username if err := db.Redis.Set(db.RedisCtx, key, data, 0).Err(); err != nil { t.Fatalf("setLivreurStatus: %v", err) } t.Cleanup(func() { db.Redis.Del(db.RedisCtx, key) }) } func setDeliveryModeSettings(t *testing.T, mode models.DeliveryModeConfig) { t.Helper() data, err := json.Marshal(mode) if err != nil { t.Fatalf("marshal delivery_mode: %v", err) } if err := testDB.GDB.Exec( `INSERT INTO app_settings (key, value) VALUES ('delivery_mode', ?) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`, string(data), ).Error; err != nil { t.Fatalf("setDeliveryModeSettings: %v", err) } t.Cleanup(func() { testDB.GDB.Exec(`DELETE FROM app_settings WHERE key = 'delivery_mode'`) }) } func containsUsername(list []string, username string) bool { for _, u := range list { if u == username { return true } } return false } // ── GetCommandCategories ───────────────────────────────────────────────────── func TestGetCommandCategories_ReturnsDistinctProductCategories(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_categories") productA := newTestProductWithCategory(t, "CatA", "cat_a", 10) productB := newTestProductWithCategory(t, "CatB", "cat_b", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productA, 1, 10) testDB.GDB.Exec( `INSERT INTO command_items (command_id, product_id, produit, quantite, prix, status) VALUES (?, ?, 'item2', 1, 10, 'pending')`, cmdID, productB, ) categories, err := testDB.GetCommandCategories(cmdID) if err != nil { t.Fatalf("GetCommandCategories: %v", err) } if len(categories) != 2 || !containsUsername(categories, "cat_a") || !containsUsername(categories, "cat_b") { t.Errorf("catégories: got=%v want=[cat_a cat_b]", categories) } } // ── GetEligibleDeliverymenForCommand ───────────────────────────────────────── func TestGetEligibleDeliverymenForCommand_SingleModeReturnsAllActive(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_single") productID := newTestProductWithCategory(t, "Single", "cat_a", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) livreurA := testUserPrefix + "delivmode_single_a" livreurB := testUserPrefix + "delivmode_single_b" setLivreurStatus(t, livreurA, "available") setLivreurStatus(t, livreurB, "available") setDeliveryModeSettings(t, models.DeliveryModeConfig{Mode: "single"}) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if !containsUsername(eligible, livreurA) || !containsUsername(eligible, livreurB) { t.Errorf("mode single doit renvoyer tous les livreurs actifs: got=%v", eligible) } } func TestGetEligibleDeliverymenForCommand_CategoryBasedFiltersToMatchingRoute(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_filter") productID := newTestProductWithCategory(t, "Filter", "cat_a", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) livreurA := testUserPrefix + "delivmode_filter_a" livreurB := testUserPrefix + "delivmode_filter_b" setLivreurStatus(t, livreurA, "available") setLivreurStatus(t, livreurB, "available") setDeliveryModeSettings(t, models.DeliveryModeConfig{ Mode: "category_based", CategoryRoutes: []models.CategoryRoute{ {DeliverymanUsername: livreurA, Categories: []string{"cat_a"}}, {DeliverymanUsername: livreurB, Categories: []string{"cat_b"}}, }, }) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if !containsUsername(eligible, livreurA) { t.Errorf("livreurA (cat_a) doit être éligible: got=%v", eligible) } if containsUsername(eligible, livreurB) { t.Errorf("livreurB (cat_b, non commandée) ne doit pas être éligible: got=%v", eligible) } } // Cas limite documenté explicitement dans le modèle métier : une commande // mixte (catégories relevant de livreurs différents) doit renvoyer l'UNION // des livreurs éligibles, pas une intersection (aucun livreur unique ne gère // forcément toutes les catégories à la fois). func TestGetEligibleDeliverymenForCommand_MixedCategoryCommand_ReturnsUnion(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_mixed") productA := newTestProductWithCategory(t, "MixedA", "cat_a", 10) productB := newTestProductWithCategory(t, "MixedB", "cat_b", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productA, 1, 10) testDB.GDB.Exec( `INSERT INTO command_items (command_id, product_id, produit, quantite, prix, status) VALUES (?, ?, 'item2', 1, 10, 'pending')`, cmdID, productB, ) livreurA := testUserPrefix + "delivmode_mixed_a" livreurB := testUserPrefix + "delivmode_mixed_b" setLivreurStatus(t, livreurA, "available") setLivreurStatus(t, livreurB, "available") setDeliveryModeSettings(t, models.DeliveryModeConfig{ Mode: "category_based", CategoryRoutes: []models.CategoryRoute{ {DeliverymanUsername: livreurA, Categories: []string{"cat_a"}}, {DeliverymanUsername: livreurB, Categories: []string{"cat_b"}}, }, }) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if !containsUsername(eligible, livreurA) || !containsUsername(eligible, livreurB) { t.Errorf("commande mixte cat_a+cat_b doit renvoyer l'union des deux livreurs: got=%v", eligible) } } func TestGetEligibleDeliverymenForCommand_NoRouteMatchesFallsBackToAllActive(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_nomatch") productID := newTestProductWithCategory(t, "NoMatch", "cat_c", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) livreurA := testUserPrefix + "delivmode_nomatch_a" setLivreurStatus(t, livreurA, "available") setDeliveryModeSettings(t, models.DeliveryModeConfig{ Mode: "category_based", CategoryRoutes: []models.CategoryRoute{ {DeliverymanUsername: livreurA, Categories: []string{"cat_a"}}, // ne couvre pas cat_c }, }) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if !containsUsername(eligible, livreurA) { t.Errorf("aucune route ne couvre cat_c -> repli sur tous les livreurs actifs: got=%v", eligible) } } func TestGetEligibleDeliverymenForCommand_EmptyCategoryRoutesFallsBackToAllActive(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_emptyroutes") productID := newTestProductWithCategory(t, "EmptyRoutes", "cat_a", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) livreurA := testUserPrefix + "delivmode_emptyroutes_a" setLivreurStatus(t, livreurA, "available") setDeliveryModeSettings(t, models.DeliveryModeConfig{Mode: "category_based", CategoryRoutes: []models.CategoryRoute{}}) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if !containsUsername(eligible, livreurA) { t.Errorf("category_based sans route configurée -> repli sur tous les livreurs actifs: got=%v", eligible) } } func TestGetEligibleDeliverymenForCommand_OfflineLivreurNeverEligible(t *testing.T) { cleanupStockTestData(t) username := newTestClient(t, "delivmode_offline") productID := newTestProductWithCategory(t, "Offline", "cat_a", 10) cmdID := newTestCommandWithItem(t, username, "pending", "", productID, 1, 10) onlineLivreur := testUserPrefix + "delivmode_offline_online" offlineLivreur := testUserPrefix + "delivmode_offline_offline" setLivreurStatus(t, onlineLivreur, "available") setLivreurStatus(t, offlineLivreur, "offline") setDeliveryModeSettings(t, models.DeliveryModeConfig{Mode: "single"}) eligible, err := testDB.GetEligibleDeliverymenForCommand(cmdID) if err != nil { t.Fatalf("GetEligibleDeliverymenForCommand: %v", err) } if containsUsername(eligible, offlineLivreur) { t.Errorf("un livreur offline ne doit jamais être éligible: got=%v", eligible) } if !containsUsername(eligible, onlineLivreur) { t.Errorf("le livreur en ligne doit être éligible: got=%v", eligible) } }