chore: build
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gestion/db"
|
||||
"gestion/handlers"
|
||||
"gestion/models"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// configurePromotionSettings applique les settings donnés (avec Promotions)
|
||||
// via testDB.UpdateSettings, comme le ferait l'admin — testDB.UpdateSettings
|
||||
// normalise déjà les slices nil, donc ce helper reste minimal.
|
||||
func configurePromotionSettings(t *testing.T, settings models.AppSettings) {
|
||||
t.Helper()
|
||||
if err := testDB.UpdateSettings(settings); err != nil {
|
||||
t.Fatalf("UpdateSettings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Persistance des settings (save→reload) ──────────────────────────────────
|
||||
|
||||
func TestUpdateSettings_PromotionsRoundTrip(t *testing.T) {
|
||||
resetSettingsAfterTest(t)
|
||||
|
||||
s := db.DefaultSettings()
|
||||
s.PromotionsEnabled = true
|
||||
s.Promotions = []models.CategoryPromotionConfig{
|
||||
{
|
||||
Category: "test",
|
||||
DiscountPercent: 15.5,
|
||||
AllProducts: false,
|
||||
Products: []models.PromotionProductQuantity{
|
||||
{ProductID: 111, Quantity: 2},
|
||||
{ProductID: 222, Quantity: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
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.PromotionsEnabled {
|
||||
t.Fatal("promotions_enabled devrait être true après reload")
|
||||
}
|
||||
if len(loaded.Promotions) != 1 {
|
||||
t.Fatalf("promotions: got=%d want=1: %+v", len(loaded.Promotions), loaded.Promotions)
|
||||
}
|
||||
promo := loaded.Promotions[0]
|
||||
if promo.Category != "test" || promo.DiscountPercent != 15.5 {
|
||||
t.Errorf("promo mal persistée: got=%+v", promo)
|
||||
}
|
||||
if len(promo.Products) != 2 || promo.Products[0].ProductID != 111 || promo.Products[0].Quantity != 2 {
|
||||
t.Errorf("products mal persistés: got=%+v", promo.Products)
|
||||
}
|
||||
|
||||
// Désactivation : doit persister à false, pas de résurrection (même
|
||||
// classe de bug que TestUpdateSettings_DisablingPointsRewardPersistsAsNil).
|
||||
s.PromotionsEnabled = false
|
||||
if err := testDB.UpdateSettings(s); 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.PromotionsEnabled {
|
||||
t.Error("promotions_enabled devrait rester false après désactivation")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Résolution de la réduction (logique pure) ───────────────────────────────
|
||||
|
||||
func TestResolvePromotionDiscount_MatchesAllProductsAtConfiguredQuantity(t *testing.T) {
|
||||
settings := &models.AppSettings{
|
||||
PromotionsEnabled: true,
|
||||
Promotions: []models.CategoryPromotionConfig{
|
||||
{Category: "fleurs", DiscountPercent: 20, AllProducts: true, Quantity: 5},
|
||||
},
|
||||
}
|
||||
discount, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 5)
|
||||
if !ok || discount != 20 {
|
||||
t.Errorf("got discount=%.2f ok=%v want=20/true", discount, ok)
|
||||
}
|
||||
// Mauvaise quantité : pas de promo.
|
||||
if _, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 3); ok {
|
||||
t.Error("ne devrait pas matcher une quantité différente de celle configurée")
|
||||
}
|
||||
// Mauvaise catégorie : pas de promo.
|
||||
if _, ok := db.ResolvePromotionDiscount(settings, 42, "autre", 5); ok {
|
||||
t.Error("ne devrait pas matcher une catégorie différente")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePromotionDiscount_DisabledReturnsNoDiscount(t *testing.T) {
|
||||
settings := &models.AppSettings{
|
||||
PromotionsEnabled: false,
|
||||
Promotions: []models.CategoryPromotionConfig{
|
||||
{Category: "fleurs", DiscountPercent: 20, AllProducts: true, Quantity: 5},
|
||||
},
|
||||
}
|
||||
if _, ok := db.ResolvePromotionDiscount(settings, 42, "fleurs", 5); ok {
|
||||
t.Error("aucune promo ne doit s'appliquer si promotions_enabled = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePromotionDiscount_PerProductSelection(t *testing.T) {
|
||||
settings := &models.AppSettings{
|
||||
PromotionsEnabled: true,
|
||||
Promotions: []models.CategoryPromotionConfig{
|
||||
{
|
||||
Category: "fleurs",
|
||||
AllProducts: false,
|
||||
Products: []models.PromotionProductQuantity{
|
||||
{ProductID: 1, Quantity: 2},
|
||||
},
|
||||
DiscountPercent: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
if discount, ok := db.ResolvePromotionDiscount(settings, 1, "fleurs", 2); !ok || discount != 10 {
|
||||
t.Errorf("produit sélectionné à la bonne quantité: got discount=%.2f ok=%v", discount, ok)
|
||||
}
|
||||
if _, ok := db.ResolvePromotionDiscount(settings, 1, "fleurs", 3); ok {
|
||||
t.Error("mauvaise quantité pour ce produit : ne doit pas matcher")
|
||||
}
|
||||
if _, ok := db.ResolvePromotionDiscount(settings, 2, "fleurs", 2); ok {
|
||||
t.Error("produit non sélectionné : ne doit pas matcher")
|
||||
}
|
||||
}
|
||||
|
||||
// ── AddToBasket applique réellement la réduction au prix facturé ───────────
|
||||
|
||||
func TestAddToBasket_AppliesPromotionDiscount(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
resetSettingsAfterTest(t)
|
||||
username := newTestClient(t, "promo_basket_applies")
|
||||
productID := newTestProduct(t, "PromoBasketApplies", 10)
|
||||
// newTestProduct crée un palier quantity=1 à 10.00€ dans la catégorie "test".
|
||||
|
||||
s := db.DefaultSettings()
|
||||
s.PromotionsEnabled = true
|
||||
s.Promotions = []models.CategoryPromotionConfig{
|
||||
{Category: "test", DiscountPercent: 20, AllProducts: true, Quantity: 1},
|
||||
}
|
||||
configurePromotionSettings(t, s)
|
||||
|
||||
basket, err := testDB.AddToBasket(username, productID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("AddToBasket: %v", err)
|
||||
}
|
||||
if basket.Price != 8.0 {
|
||||
t.Errorf("prix attendu = 10€ - 20%% = 8.00€: got=%.2f", basket.Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToBasket_NoDiscountWhenPromotionsDisabled(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
resetSettingsAfterTest(t)
|
||||
username := newTestClient(t, "promo_basket_disabled")
|
||||
productID := newTestProduct(t, "PromoBasketDisabled", 10)
|
||||
|
||||
s := db.DefaultSettings()
|
||||
s.PromotionsEnabled = false
|
||||
s.Promotions = []models.CategoryPromotionConfig{
|
||||
{Category: "test", DiscountPercent: 20, AllProducts: true, Quantity: 1},
|
||||
}
|
||||
configurePromotionSettings(t, s)
|
||||
|
||||
basket, err := testDB.AddToBasket(username, productID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("AddToBasket: %v", err)
|
||||
}
|
||||
if basket.Price != 10.0 {
|
||||
t.Errorf("promotions désactivées: le prix catalogue plein doit s'appliquer: got=%.2f want=10.00", basket.Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddToBasket_NoDiscountForDifferentProductSelection(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
resetSettingsAfterTest(t)
|
||||
username := newTestClient(t, "promo_basket_other_product")
|
||||
promotedID := newTestProduct(t, "PromoBasketOtherPromoted", 10)
|
||||
otherID := newTestProduct(t, "PromoBasketOtherPlain", 10)
|
||||
|
||||
s := db.DefaultSettings()
|
||||
s.PromotionsEnabled = true
|
||||
s.Promotions = []models.CategoryPromotionConfig{
|
||||
{
|
||||
Category: "test",
|
||||
AllProducts: false,
|
||||
Products: []models.PromotionProductQuantity{{ProductID: promotedID, Quantity: 1}},
|
||||
DiscountPercent: 50,
|
||||
},
|
||||
}
|
||||
configurePromotionSettings(t, s)
|
||||
|
||||
promotedBasket, err := testDB.AddToBasket(username, promotedID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("AddToBasket (promu): %v", err)
|
||||
}
|
||||
if promotedBasket.Price != 5.0 {
|
||||
t.Errorf("produit promu: prix attendu = 10€ - 50%% = 5.00€: got=%.2f", promotedBasket.Price)
|
||||
}
|
||||
|
||||
otherBasket, err := testDB.AddToBasket(username, otherID, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("AddToBasket (autre): %v", err)
|
||||
}
|
||||
if otherBasket.Price != 10.0 {
|
||||
t.Errorf("produit non sélectionné dans la promo: prix plein attendu=10.00: got=%.2f", otherBasket.Price)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Affichage catalogue : le prix promo est annoté sur le palier concerné ──
|
||||
|
||||
func TestGetProductByID_AnnotatesPromoPriceOnMatchingTier(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
resetSettingsAfterTest(t)
|
||||
productID := newTestProduct(t, "PromoDisplayAnnotated", 10)
|
||||
|
||||
s := db.DefaultSettings()
|
||||
s.PromotionsEnabled = true
|
||||
s.Promotions = []models.CategoryPromotionConfig{
|
||||
{Category: "test", DiscountPercent: 25, AllProducts: true, Quantity: 1},
|
||||
}
|
||||
configurePromotionSettings(t, s)
|
||||
|
||||
idStr := strconv.Itoa(productID)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/products/"+idStr, nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = req
|
||||
c.Set("database", testDB)
|
||||
c.Params = gin.Params{{Key: "id", Value: idStr}}
|
||||
c.Set("role", "client")
|
||||
handlers.GetProductByID(c)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status HTTP: got=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data models.Product `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("décodage réponse: %v body=%s", err, rec.Body.String())
|
||||
}
|
||||
if len(resp.Data.Prices) != 1 {
|
||||
t.Fatalf("attendu 1 palier de prix: got=%+v", resp.Data.Prices)
|
||||
}
|
||||
tier := resp.Data.Prices[0]
|
||||
if tier.PromoPrice == nil {
|
||||
t.Fatal("PromoPrice devrait être renseigné pour ce palier couvert par la promo")
|
||||
}
|
||||
if *tier.PromoPrice != 7.5 {
|
||||
t.Errorf("promo_price attendu = 10€ - 25%% = 7.50€: got=%.2f", *tier.PromoPrice)
|
||||
}
|
||||
if tier.PromoPercent != 25 {
|
||||
t.Errorf("promo_percent attendu=25: got=%.2f", tier.PromoPercent)
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,9 @@ func claimRewardContext(username string, body []byte) (*gin.Context, *httptest.R
|
||||
}
|
||||
|
||||
// configureRewardSettings applique la récompense donnée, avec pool_0 mappé
|
||||
// sur la catégorie "test" — nécessaire pour que eligibleRewardProducts
|
||||
// sur la catégorie "test" — nécessaire pour que resolveCategoryRewardCandidates
|
||||
// (qui croise pool.Categories et reward.CategoryConfigs) considère les
|
||||
// reward_items comme éligibles.
|
||||
// produits de la catégorie comme éligibles.
|
||||
func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
|
||||
t.Helper()
|
||||
settings := db.DefaultSettings()
|
||||
@@ -39,17 +39,20 @@ func configureRewardSettings(t *testing.T, reward *models.PointsReward) {
|
||||
}
|
||||
|
||||
// Flux complet réel : POST /points/claim avec un seuil atteint doit ajouter
|
||||
// le produit récompense configuré au panier et décompter la récompense.
|
||||
// le produit récompense configuré au panier et décompter la récompense. Le
|
||||
// produit éligible et sa quantité sont désormais définis directement dans le
|
||||
// bloc catégorie (RewardCategoryConfig), plus de liste "reward_items" à part.
|
||||
func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_flow")
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPFlow", 5)
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit offert",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
Threshold: 20,
|
||||
Description: "Un produit offert",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "free_product", AllProducts: true, Quantity: 1},
|
||||
},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
@@ -85,9 +88,9 @@ func TestClaimMyReward_HTTPFlow_AddsRewardToBasketAndDecrementsAvailable(t *test
|
||||
}
|
||||
}
|
||||
|
||||
// Catégorie configurée en "half_price_product" : le produit récompense doit
|
||||
// être ajouté au panier à 50% du prix catalogue actif (pas 0€, pas le prix
|
||||
// indicatif RewardItem.Price saisi par l'admin).
|
||||
// Catégorie configurée en "half_price_product" avec quantité=1 : le produit
|
||||
// récompense doit être ajouté au panier à 50% du prix catalogue actif pour
|
||||
// cette quantité (palier ≤ 1), pas 0€.
|
||||
func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPrice(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_halfprice")
|
||||
@@ -95,10 +98,11 @@ func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPri
|
||||
// newTestProduct crée un prix actif de 10.00€ pour quantity=1 (voir tests/main_test.go).
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit à moitié prix",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "half_price_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 999}}, // Price indicatif, doit être ignoré
|
||||
Threshold: 20,
|
||||
Description: "Un produit à moitié prix",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "half_price_product", AllProducts: true, Quantity: 1},
|
||||
},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
@@ -119,15 +123,60 @@ func TestClaimMyReward_HTTPFlow_HalfPriceCategoryChargesFiftyPercentOfCatalogPri
|
||||
}
|
||||
}
|
||||
|
||||
// La quantité configurée dans le bloc catégorie détermine le palier de prix
|
||||
// utilisé pour le calcul du -50% (ex: 30€ le palier quantity=1 → 15€ facturé),
|
||||
// pas un prix indicatif saisi ailleurs.
|
||||
func TestClaimMyReward_HTTPFlow_HalfPriceUsesConfiguredQuantityForPriceTier(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_halfprice_qty")
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPHalfPriceQty", 20)
|
||||
// Ajoute un palier quantity=3 à 30€ (en plus du palier quantity=1 à 10€ créé par newTestProduct).
|
||||
if err := testDB.GDB.Exec(
|
||||
`INSERT INTO product_prices (product_id, quantity, price, active_price) VALUES (?, 3, 30.00, true)`,
|
||||
rewardProductID,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("création palier de prix supplémentaire: %v", err)
|
||||
}
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Un produit à moitié prix, quantité 3",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "half_price_product", AllProducts: true, Quantity: 3},
|
||||
},
|
||||
})
|
||||
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) != 1 || rows[0].ProductID != rewardProductID {
|
||||
t.Fatalf("le produit récompense doit être dans le panier: %+v", rows)
|
||||
}
|
||||
if rows[0].Quantity != 3 {
|
||||
t.Errorf("la quantité en panier doit être celle configurée pour la catégorie: got=%.2f want=3", rows[0].Quantity)
|
||||
}
|
||||
if rows[0].Price != 15.0 {
|
||||
t.Errorf("palier quantity=3 à 30€ : prix attendu = 50%% = 15.00€: got=%.2f", rows[0].Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_below")
|
||||
rewardProductID := newTestProduct(t, "RewardHTTPBelow", 5)
|
||||
newTestProduct(t, "RewardHTTPBelow", 5)
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", AllProducts: true}},
|
||||
RewardItems: []models.RewardItem{{ProductID: rewardProductID, Quantity: 1, Price: 12}},
|
||||
Threshold: 20,
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "free_product", AllProducts: true, Quantity: 1},
|
||||
},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 5)
|
||||
|
||||
@@ -140,22 +189,21 @@ func TestClaimMyReward_HTTPFlow_RejectsWhenBelowThreshold(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Si un item récompense configuré par l'admin pointe vers un produit
|
||||
// supprimé/inexistant, la réclamation entière doit échouer — la récompense
|
||||
// ne doit pas être consommée sans qu'aucun produit ne soit livré au client
|
||||
// (ClaimPoolReward + AddRewardsToBasket sont maintenant dans la même
|
||||
// transaction via ClaimPoolRewardAndAddToBasket).
|
||||
// Si un produit configuré par l'admin (via Products explicite) pointe vers
|
||||
// un produit supprimé/inexistant, la réclamation entière doit échouer — la
|
||||
// récompense ne doit pas être consommée sans qu'aucun produit ne soit livré
|
||||
// au client (ClaimPoolReward + AddRewardsToBasket sont dans la même
|
||||
// transaction via ClaimPoolRewardAndAddToBasket ; la contrainte de clé
|
||||
// étrangère sur baskets.product_id fait échouer l'insertion).
|
||||
func TestClaimMyReward_HTTPFlow_FailsAtomicallyWhenProductMissing(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_missing_product")
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
// ProductIDs explicite (pas AllProducts) : le produit n'existe pas en
|
||||
// base, donc il n'apparaîtrait jamais dans productCategories et ne
|
||||
// serait jamais éligible via une correspondance AllProducts.
|
||||
CategoryConfigs: []models.RewardCategoryConfig{{Category: "test", Type: "free_product", ProductIDs: []int{999999999}}},
|
||||
RewardItems: []models.RewardItem{{ProductID: 999999999, Quantity: 1, Price: 12}}, // produit inexistant
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "free_product", Products: []models.RewardProductQuantity{{ProductID: 999999999, Quantity: 1}}},
|
||||
},
|
||||
})
|
||||
setClientPoolPoints(t, username, "pool_0", 20)
|
||||
|
||||
@@ -175,3 +223,114 @@ 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"])
|
||||
}
|
||||
}
|
||||
|
||||
// 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 et sa propre quantité. Un seul claim
|
||||
// doit alors ajouter les deux produits au panier, chacun tarifé selon son
|
||||
// propre type et sa propre quantité.
|
||||
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", Products: []models.RewardProductQuantity{{ProductID: freeProductID, Quantity: 1}}},
|
||||
{Category: "test", Type: "half_price_product", Products: []models.RewardProductQuantity{{ProductID: halfProductID, Quantity: 1}}},
|
||||
},
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Quand une catégorie n'est pas configurée en "tous les produits", chaque
|
||||
// produit sélectionné a sa propre quantité (ex: produit A à 2g offerts,
|
||||
// produit B à 1g offert, tous deux dans la même catégorie et le même type).
|
||||
func TestClaimMyReward_HTTPFlow_PerProductQuantityWithinSameCategoryAndType(t *testing.T) {
|
||||
cleanupStockTestData(t)
|
||||
username := newTestClient(t, "reward_http_per_product_qty")
|
||||
productA := newTestProduct(t, "RewardHTTPPerProductA", 5)
|
||||
productB := newTestProduct(t, "RewardHTTPPerProductB", 5)
|
||||
// newTestProduct crée les deux produits dans la catégorie "test".
|
||||
|
||||
configureRewardSettings(t, &models.PointsReward{
|
||||
Threshold: 20,
|
||||
Description: "Produit A 2g offert, produit B 1g offert",
|
||||
CategoryConfigs: []models.RewardCategoryConfig{
|
||||
{Category: "test", Type: "free_product", Products: []models.RewardProductQuantity{
|
||||
{ProductID: productA, Quantity: 2},
|
||||
{ProductID: productB, Quantity: 1},
|
||||
}},
|
||||
},
|
||||
})
|
||||
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 rowA, rowB *rewardBasketRow
|
||||
for i := range rows {
|
||||
switch rows[i].ProductID {
|
||||
case productA:
|
||||
rowA = &rows[i]
|
||||
case productB:
|
||||
rowB = &rows[i]
|
||||
}
|
||||
}
|
||||
if rowA == nil || rowB == nil {
|
||||
t.Fatalf("les deux produits attendus doivent être présents: %+v", rows)
|
||||
}
|
||||
if rowA.Quantity != 2 {
|
||||
t.Errorf("produit A: quantité attendue=2, got=%.2f", rowA.Quantity)
|
||||
}
|
||||
if rowB.Quantity != 1 {
|
||||
t.Errorf("produit B: quantité attendue=1, got=%.2f", rowB.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -40,6 +41,41 @@ func basketRewardItems(t *testing.T, username string) []rewardBasketRow {
|
||||
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 ──────────────────────────
|
||||
|
||||
func TestClaimPoolReward_BelowThresholdFails(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user