This commit is contained in:
@@ -72,19 +72,19 @@ func DefaultSettings() models.AppSettings {
|
|||||||
Mode: "single",
|
Mode: "single",
|
||||||
CategoryRoutes: []models.CategoryRoute{},
|
CategoryRoutes: []models.CategoryRoute{},
|
||||||
},
|
},
|
||||||
AdminColorPrimary: "#7c3aed",
|
AdminColorPrimary: "#7c3aed",
|
||||||
AdminColorSecondary: "#000000",
|
AdminColorSecondary: "#000000",
|
||||||
AdminColorSuccess: "#4ade80",
|
AdminColorSuccess: "#4ade80",
|
||||||
AdminColorDanger: "#ef4444",
|
AdminColorDanger: "#ef4444",
|
||||||
AdminColorWarning: "#f59e0b",
|
AdminColorWarning: "#f59e0b",
|
||||||
ClientColorPrimary: "#7c3aed",
|
ClientColorPrimary: "#7c3aed",
|
||||||
ClientColorSecondary: "#000000",
|
ClientColorSecondary: "#000000",
|
||||||
ClientColorSuccess: "#4ade80",
|
ClientColorSuccess: "#4ade80",
|
||||||
ClientColorDanger: "#ef4444",
|
ClientColorDanger: "#ef4444",
|
||||||
ClientColorWarning: "#f59e0b",
|
ClientColorWarning: "#f59e0b",
|
||||||
ClientTitleGradientFrom: "#a78bfa",
|
ClientTitleGradientFrom: "#a78bfa",
|
||||||
ClientTitleGradientTo: "#22d3ee",
|
ClientTitleGradientTo: "#22d3ee",
|
||||||
DeliverySchedule: DefaultDeliverySchedule(),
|
DeliverySchedule: DefaultDeliverySchedule(),
|
||||||
PostalZones: []models.PostalZone{
|
PostalZones: []models.PostalZone{
|
||||||
{Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}},
|
{Name: "Zone 30€", MinAmount: 30, Codes: []string{"44000", "44100", "44200", "44300"}},
|
||||||
{Name: "Zone 50€", MinAmount: 50, Codes: []string{
|
{Name: "Zone 50€", MinAmount: 50, Codes: []string{
|
||||||
@@ -115,6 +115,11 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
|
|||||||
switch row.Key {
|
switch row.Key {
|
||||||
case "penalties_enabled":
|
case "penalties_enabled":
|
||||||
settings.PenaltiesEnabled = row.Value == "true"
|
settings.PenaltiesEnabled = row.Value == "true"
|
||||||
|
case "penalty_tiers":
|
||||||
|
var tiers []models.PenaltyTier
|
||||||
|
if err := json.Unmarshal([]byte(row.Value), &tiers); err == nil {
|
||||||
|
settings.PenaltyTiers = tiers
|
||||||
|
}
|
||||||
case "show_amende_score":
|
case "show_amende_score":
|
||||||
settings.ShowAmendeScore = row.Value == "true"
|
settings.ShowAmendeScore = row.Value == "true"
|
||||||
case "points_enabled":
|
case "points_enabled":
|
||||||
@@ -221,6 +226,14 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
|||||||
return "false"
|
return "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.PenaltyTiers == nil {
|
||||||
|
s.PenaltyTiers = []models.PenaltyTier{}
|
||||||
|
}
|
||||||
|
tiersJSON, err := json.Marshal(s.PenaltyTiers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("erreur sérialisation penalty_tiers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if s.PointsPools == nil {
|
if s.PointsPools == nil {
|
||||||
s.PointsPools = []models.PointsPool{}
|
s.PointsPools = []models.PointsPool{}
|
||||||
}
|
}
|
||||||
@@ -277,6 +290,7 @@ func (d *Database) UpdateSettings(s models.AppSettings) error {
|
|||||||
}
|
}
|
||||||
pairs := [][2]string{
|
pairs := [][2]string{
|
||||||
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
|
{"penalties_enabled", boolStr(s.PenaltiesEnabled)},
|
||||||
|
{"penalty_tiers", string(tiersJSON)},
|
||||||
{"show_amende_score", boolStr(s.ShowAmendeScore)},
|
{"show_amende_score", boolStr(s.ShowAmendeScore)},
|
||||||
{"points_enabled", boolStr(s.PointsEnabled)},
|
{"points_enabled", boolStr(s.PointsEnabled)},
|
||||||
{"points_pools", string(poolsJSON)},
|
{"points_pools", string(poolsJSON)},
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gestion/db"
|
||||||
|
"gestion/models"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user