package tests import ( "bytes" "encoding/json" "gestion/db" "gestion/handlers" "gestion/models" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" ) // resetSettingsAfterTest restaure les settings par défaut à la fin du test — // AppSettings est un état global partagé (une seule ligne par clé dans // app_settings), donc un test qui le modifie ne doit pas laisser de résidu // pour les tests suivants (ex: DeliveryMode utilisé par d'autres suites). func resetSettingsAfterTest(t *testing.T) { t.Helper() t.Cleanup(func() { if err := testDB.UpdateSettings(db.DefaultSettings()); err != nil { t.Logf("⚠️ resetSettingsAfterTest: restauration des settings par défaut échouée: %v", err) } }) } // ── Bascules booléennes (activer/désactiver une option) ───────────────────── // // Régression visée : chaque option doit persister à sa valeur exacte après un // cycle save→reload, dans les deux sens (activation ET désactivation) — voir // TestUpdateSettings_DisablingPointsRewardPersistsAsNil pour un cas où la // désactivation ne persistait pas correctement. func TestUpdateSettings_DisablingBooleanTogglesPersists(t *testing.T) { resetSettingsAfterTest(t) set := func(v bool) models.AppSettings { s := db.DefaultSettings() s.PenaltiesEnabled = v s.ShowAmendeScore = v s.PointsEnabled = v s.ReferralEnabled = v s.CryptoPaymentEnabled = v s.CryptoOnly = v s.TelegramNotificationsEnabled = v s.Telegram2FAEnabled = v return s } assertAll := func(t *testing.T, want bool) { t.Helper() loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } checks := map[string]bool{ "penalties_enabled": loaded.PenaltiesEnabled, "show_amende_score": loaded.ShowAmendeScore, "points_enabled": loaded.PointsEnabled, "referral_enabled": loaded.ReferralEnabled, "crypto_payment_enabled": loaded.CryptoPaymentEnabled, "crypto_only": loaded.CryptoOnly, "telegram_notifications_enabled": loaded.TelegramNotificationsEnabled, "telegram_2fa_enabled": loaded.Telegram2FAEnabled, } for key, got := range checks { if got != want { t.Errorf("%s: got=%v want=%v", key, got, want) } } } if err := testDB.UpdateSettings(set(true)); err != nil { t.Fatalf("UpdateSettings (activation): %v", err) } assertAll(t, true) if err := testDB.UpdateSettings(set(false)); err != nil { t.Fatalf("UpdateSettings (désactivation): %v", err) } assertAll(t, false) } // ── Options non-booléennes (hors NowPayments) ─────────────────────────────── // Le barème des amendes (penalty_tiers) est éditable dans l'admin // ("Barème des amendes") mais aucune clé "penalty_tiers" n'existe dans les // pairs persistées par UpdateSettings ni dans le switch de GetSettings — la // configuration saisie par l'admin est donc silencieusement perdue au // prochain rechargement, et retombe toujours sur le barème par défaut. func TestUpdateSettings_PenaltyTiersRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.PenaltyTiers = []models.PenaltyTier{ {MinCancel: 0, Amount: 10}, {MinCancel: 5, Amount: 999}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if len(loaded.PenaltyTiers) != 2 || loaded.PenaltyTiers[1].Amount != 999 { t.Errorf("le barème des amendes personnalisé n'a pas été persisté: got=%+v", loaded.PenaltyTiers) } } func TestUpdateSettings_ReferralAmountRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.ReferralAmount = 12.5 if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if loaded.ReferralAmount != 12.5 { t.Errorf("referral_amount: got=%.2f want=12.50", loaded.ReferralAmount) } s.ReferralAmount = 0 if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings (remise à zéro): %v", err) } loaded, err = testDB.GetSettings() if err != nil { t.Fatalf("GetSettings (remise à zéro): %v", err) } if loaded.ReferralAmount != 0 { t.Errorf("referral_amount remis à 0: got=%.2f want=0.00", loaded.ReferralAmount) } } func TestUpdateSettings_PointsPoolsRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.PointsPools = []models.PointsPool{ { Key: "pool_custom", Name: "Pool Custom", Categories: []string{"catA", "catB"}, Tiers: []models.PointsTier{{Min: 10, Max: 20, Points: 7}}, }, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if len(loaded.PointsPools) != 1 || loaded.PointsPools[0].Key != "pool_custom" || len(loaded.PointsPools[0].Categories) != 2 || loaded.PointsPools[0].Tiers[0].Points != 7 { t.Errorf("points_pools personnalisé mal persisté: got=%+v", loaded.PointsPools) } } func TestUpdateSettings_DeliveryScheduleRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.DeliverySchedule.Monday = models.DaySchedule{Enabled: false, OpenTime: "10:00", CloseTime: "18:00"} if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if loaded.DeliverySchedule.Monday.Enabled != false || loaded.DeliverySchedule.Monday.OpenTime != "10:00" || loaded.DeliverySchedule.Monday.CloseTime != "18:00" { t.Errorf("delivery_schedule.monday mal persisté: got=%+v", loaded.DeliverySchedule.Monday) } } func TestUpdateSettings_PostalZonesRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.PostalZones = []models.PostalZone{ {Name: "Zone Test", MinAmount: 42, Codes: []string{"11111", "22222"}}, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if len(loaded.PostalZones) != 1 || loaded.PostalZones[0].MinAmount != 42 || len(loaded.PostalZones[0].Codes) != 2 { t.Errorf("postal_zones mal persisté: got=%+v", loaded.PostalZones) } } func TestUpdateSettings_DeliveryModeRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.DeliveryMode = models.DeliveryModeConfig{ Mode: "category_based", CategoryRoutes: []models.CategoryRoute{ {DeliverymanUsername: "livreur_test", Categories: []string{"catA"}}, }, } if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if loaded.DeliveryMode.Mode != "category_based" || len(loaded.DeliveryMode.CategoryRoutes) != 1 || loaded.DeliveryMode.CategoryRoutes[0].DeliverymanUsername != "livreur_test" { t.Errorf("delivery_mode mal persisté: got=%+v", loaded.DeliveryMode) } // Repasser en mode "single" avec une liste vide doit aussi persister // correctement (pas de résidu de l'ancienne liste category_routes). s.DeliveryMode = models.DeliveryModeConfig{Mode: "single", CategoryRoutes: []models.CategoryRoute{}} if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings (retour single): %v", err) } loaded, err = testDB.GetSettings() if err != nil { t.Fatalf("GetSettings (retour single): %v", err) } if loaded.DeliveryMode.Mode != "single" || len(loaded.DeliveryMode.CategoryRoutes) != 0 { t.Errorf("delivery_mode retour à single mal persisté: got=%+v", loaded.DeliveryMode) } } func TestUpdateSettings_ShopAndTelegramTextFieldsRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.ShopName = "Ma Boutique Test" s.TelegramBotToken = "123456:ABC-test-token" s.TelegramBotUsername = "mon_bot_test" if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if loaded.ShopName != "Ma Boutique Test" { t.Errorf("shop_name: got=%q want=%q", loaded.ShopName, "Ma Boutique Test") } if loaded.TelegramBotToken != "123456:ABC-test-token" { t.Errorf("telegram_bot_token: got=%q", loaded.TelegramBotToken) } if loaded.TelegramBotUsername != "mon_bot_test" { t.Errorf("telegram_bot_username: got=%q", loaded.TelegramBotUsername) } // Effacer le token/username (chaîne vide) doit aussi persister tel quel — // contrairement à contact_telegram qui a un repli explicite non-vide. s.TelegramBotToken = "" s.TelegramBotUsername = "" if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings (effacement): %v", err) } loaded, err = testDB.GetSettings() if err != nil { t.Fatalf("GetSettings (effacement): %v", err) } if loaded.TelegramBotToken != "" || loaded.TelegramBotUsername != "" { t.Errorf("token/username effacés devraient rester vides: got token=%q username=%q", loaded.TelegramBotToken, loaded.TelegramBotUsername) } } func TestUpdateSettings_ColorAndGradientFieldsRoundTrip(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.AdminColorPrimary = "#111111" s.ClientColorDanger = "#222222" s.ClientTitleGradientFrom = "#333333" s.ClientTitleGradientTo = "#444444" if err := testDB.UpdateSettings(s); err != nil { t.Fatalf("UpdateSettings: %v", err) } loaded, err := testDB.GetSettings() if err != nil { t.Fatalf("GetSettings: %v", err) } if loaded.AdminColorPrimary != "#111111" { t.Errorf("admin_color_primary: got=%q", loaded.AdminColorPrimary) } if loaded.ClientColorDanger != "#222222" { t.Errorf("client_color_danger: got=%q", loaded.ClientColorDanger) } if loaded.ClientTitleGradientFrom != "#333333" || loaded.ClientTitleGradientTo != "#444444" { t.Errorf("client_title_gradient: got from=%q to=%q", loaded.ClientTitleGradientFrom, loaded.ClientTitleGradientTo) } } // Reproduit exactement le flux réel de l'admin : PUT /settings avec le JSON // tel qu'envoyé par le frontend (category_configs[].products, en mode // sélection), puis GET /settings pour vérifier ce qui revient — contrairement // aux autres tests de ce fichier qui appellent testDB.UpdateSettings / // GetSettings directement en Go, en contournant le binding JSON HTTP réel. func TestUpdateSettingsHTTP_CategoryConfigProductsSurviveSaveReload(t *testing.T) { resetSettingsAfterTest(t) s := db.DefaultSettings() s.PointsReward = &models.PointsReward{ Threshold: 20, CategoryConfigs: []models.RewardCategoryConfig{ { Category: "test", Type: "free_product", AllProducts: false, Products: []models.RewardProductQuantity{ {ProductID: 111, Quantity: 2}, {ProductID: 222, Quantity: 1}, }, }, }, } body, err := json.Marshal(s) if err != nil { t.Fatalf("json.Marshal: %v", err) } putReq := httptest.NewRequest(http.MethodPut, "/api/v2/admin/protected/settings", bytes.NewReader(body)) putReq.Header.Set("Content-Type", "application/json") putRec := httptest.NewRecorder() putCtx, _ := gin.CreateTestContext(putRec) putCtx.Request = putReq putCtx.Set("database", testDB) handlers.UpdateSettings(putCtx) if putRec.Code != http.StatusOK { t.Fatalf("PUT /settings: status=%d body=%s", putRec.Code, putRec.Body.String()) } getReq := httptest.NewRequest(http.MethodGet, "/api/v2/admin/protected/settings", nil) getRec := httptest.NewRecorder() getCtx, _ := gin.CreateTestContext(getRec) getCtx.Request = getReq getCtx.Set("database", testDB) handlers.GetSettings(getCtx) if getRec.Code != http.StatusOK { t.Fatalf("GET /settings: status=%d body=%s", getRec.Code, getRec.Body.String()) } var resp struct { Settings models.AppSettings `json:"settings"` } if err := json.Unmarshal(getRec.Body.Bytes(), &resp); err != nil { t.Fatalf("décodage réponse GET: %v body=%s", err, getRec.Body.String()) } if resp.Settings.PointsReward == nil { t.Fatalf("points_reward est nil après reload") } if len(resp.Settings.PointsReward.CategoryConfigs) != 1 { t.Fatalf("category_configs: got=%d want=1: %+v", len(resp.Settings.PointsReward.CategoryConfigs), resp.Settings.PointsReward.CategoryConfigs) } cfg := resp.Settings.PointsReward.CategoryConfigs[0] if len(cfg.Products) != 2 { t.Fatalf("products: got=%d want=2 (produits sélectionnés non persistés): %+v", len(cfg.Products), cfg.Products) } if cfg.Products[0].ProductID != 111 || cfg.Products[0].Quantity != 2 { t.Errorf("products[0]: got=%+v want={ProductID:111 Quantity:2}", cfg.Products[0]) } if cfg.Products[1].ProductID != 222 || cfg.Products[1].Quantity != 1 { t.Errorf("products[1]: got=%+v want={ProductID:222 Quantity:1}", cfg.Products[1]) } }