391 lines
13 KiB
Go
391 lines
13 KiB
Go
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])
|
|
}
|
|
}
|