chore: build
Backend - Build & Lint / build (push) Canceled after 0s

This commit is contained in:
Xor290
2026-08-25 18:52:55 +02:00
parent f469e52c32
commit 30eebd12d2
3 changed files with 108 additions and 3 deletions
+11 -3
View File
@@ -125,9 +125,17 @@ func (d *Database) GetSettings() (models.AppSettings, error) {
settings.PointsPools = pools settings.PointsPools = pools
} }
case "points_reward": case "points_reward":
var reward models.PointsReward // row.Value peut valoir la chaîne littérale "null" (récompense
if err := json.Unmarshal([]byte(row.Value), &reward); err == nil { // désactivée puis sauvegardée : json.Marshal(nil *PointsReward)
settings.PointsReward = &reward // produit "null"). json.Unmarshal d'un null JSON dans une valeur
// non-pointeur est un no-op sans erreur (voir doc encoding/json),
// donc sans ce garde-fou &reward pointerait vers une struct vide
// mais non-nil, et la récompense réapparaîtrait activée.
if row.Value != "null" && row.Value != "" {
var reward models.PointsReward
if err := json.Unmarshal([]byte(row.Value), &reward); err == nil {
settings.PointsReward = &reward
}
} }
case "referral_enabled": case "referral_enabled":
settings.ReferralEnabled = row.Value == "true" settings.ReferralEnabled = row.Value == "true"
@@ -175,3 +175,64 @@ func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T)
t.Errorf("la récompense ne doit PAS être consommée si le produit est introuvable: got redeemed=%d want=0", redeemed["pool_0"]) t.Errorf("la récompense ne doit PAS être consommée si le produit est introuvable: got redeemed=%d want=0", redeemed["pool_0"])
} }
} }
// Une même catégorie peut avoir les deux types de récompense actifs en
// parallèle (un lot de produits offerts + un lot de produits à -50%), chacun
// avec sa propre sélection de produits (voir eligibleRewardProducts, qui
// n'impose aucune unicité de catégorie dans CategoryConfigs). Un seul claim
// doit alors ajouter les deux produits au panier, chacun tarifé selon son
// propre type.
func TestClaimMyReward_HTTPFlow_CategoryWithBothTypesSimultaneously(t *testing.T) {
cleanupStockTestData(t)
username := newTestClient(t, "reward_http_dual_type")
freeProductID := newTestProduct(t, "RewardHTTPDualFree", 5)
halfProductID := newTestProduct(t, "RewardHTTPDualHalf", 5)
// newTestProduct crée les deux produits dans la catégorie "test", avec un
// prix actif de 10.00€ pour quantity=1 (voir tests/main_test.go).
configureRewardSettings(t, &models.PointsReward{
Threshold: 20,
Description: "Un produit offert + un produit à -50%",
CategoryConfigs: []models.RewardCategoryConfig{
{Category: "test", Type: "free_product", ProductIDs: []int{freeProductID}},
{Category: "test", Type: "half_price_product", ProductIDs: []int{halfProductID}},
},
RewardItems: []models.RewardItem{
{ProductID: freeProductID, Quantity: 1, Price: 12},
{ProductID: halfProductID, Quantity: 1, Price: 12},
},
})
setClientPoolPoints(t, username, "pool_0", 20)
body, _ := json.Marshal(map[string]string{"pool_key": "pool_0"})
c, rec := claimRewardContext(username, body)
handlers.ClaimMyReward(c)
if rec.Code != http.StatusOK {
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
}
rows := basketRewardItems(t, username)
if len(rows) != 2 {
t.Fatalf("les deux produits récompense doivent être dans le panier: %+v", rows)
}
var freeRow, halfRow *rewardBasketRow
for i := range rows {
switch rows[i].ProductID {
case freeProductID:
freeRow = &rows[i]
case halfProductID:
halfRow = &rows[i]
}
}
if freeRow == nil || halfRow == nil {
t.Fatalf("les deux produits attendus doivent être présents: %+v", rows)
}
if freeRow.Price != 0 {
t.Errorf("produit de la config free_product: le prix en panier doit être 0: got=%.2f", freeRow.Price)
}
if halfRow.Price != 5.0 {
t.Errorf("produit de la config half_price_product: prix attendu = 50%% de 10.00€ = 5.00€: got=%.2f", halfRow.Price)
}
}
+36
View File
@@ -1,6 +1,7 @@
package tests package tests
import ( import (
"gestion/db"
"gestion/models" "gestion/models"
"strings" "strings"
"sync" "sync"
@@ -40,6 +41,41 @@ func basketRewardItems(t *testing.T, username string) []rewardBasketRow {
return rows return rows
} }
// Désactiver la récompense (PointsReward = nil) puis sauvegarder ne doit pas
// la faire réapparaître activée au rechargement — régression : json.Marshal
// d'un pointeur nil produit la chaîne "null", et json.Unmarshal d'un null
// JSON dans une valeur non-pointeur est un no-op sans erreur, ce qui laissait
// settings.PointsReward pointer vers une struct vide mais non-nil.
func TestUpdateSettings_DisablingPointsRewardPersistsAsNil(t *testing.T) {
settings := db.DefaultSettings()
settings.PointsReward = &models.PointsReward{
Threshold: 20,
Description: "Un produit offert",
}
if err := testDB.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings (activation): %v", err)
}
loaded, err := testDB.GetSettings()
if err != nil {
t.Fatalf("GetSettings (activation): %v", err)
}
if loaded.PointsReward == nil {
t.Fatal("la récompense devrait être active après la première sauvegarde")
}
settings.PointsReward = nil
if err := testDB.UpdateSettings(settings); err != nil {
t.Fatalf("UpdateSettings (désactivation): %v", err)
}
loaded, err = testDB.GetSettings()
if err != nil {
t.Fatalf("GetSettings (désactivation): %v", err)
}
if loaded.PointsReward != nil {
t.Errorf("la récompense désactivée ne doit pas réapparaître après sauvegarde: got=%+v", loaded.PointsReward)
}
}
// ── ClaimPoolReward : seuil, atomicité, épuisement ────────────────────────── // ── ClaimPoolReward : seuil, atomicité, épuisement ──────────────────────────
func TestClaimPoolReward_BelowThresholdFails(t *testing.T) { func TestClaimPoolReward_BelowThresholdFails(t *testing.T) {