Aller au contenu

Chaos Métier

Le Chaos Métier injecte 16 anomalies fonctionnelles dans le parcours e-commerce. Contrairement au Chaos Performance qui dégrade l'infrastructure et au Chaos Fonctionnel qui provoque des exceptions JVM, ces anomalies touchent la logique applicative : prix, stock, commandes, livraison, fidélité, devise.

L'objectif pédagogique est aligné sur les certifications ISTQB Foundation et TMAP : apprendre à détecter et documenter des anomalies métier qui n'apparaissent pas dans les logs d'erreur classiques.

Service et endpoint

Classe : BusinessChaosService.java Controller : BusinessChaosController.java Endpoint admin : POST /api/admin/chaos/business body {"level": 0-4} Endpoint public : GET /api/chaos/public/business/{status,logs,anomalies}

Niveaux

Niveau Label Anomalies actives Cumul
0 Désactivé aucune 0
1 Junior A1 – A3 3
2 Confirmé A1 – A7 7
3 Expert A1 – A11 11
4 Master A1 – A16 16

Règle d'or

Avec les bonnes données de test, une commande doit toujours pouvoir aboutir quel que soit le niveau de chaos métier actif. Aucune anomalie business ne lève d'exception bloquante — elles introduisent des écarts silencieux dans les calculs, les états ou les retours d'API.

Niveau 1 — Junior (A1 – A3)

A1 — TVA 19,6 % au lieu de 20 %

Méthode : getTvaRate(String userEmail) / applyTva(BigDecimal, String) Compteur : chaos.business.a1.tva

Le taux de TVA appliqué au calcul TTC passe de 20 % à 19,6 % (l'ancien taux français pré-2014). L'écart est faible — 0,33 % sur le montant TTC — ce qui rend l'anomalie difficile à détecter par inspection visuelle. Sur une commande de 120 € TTC, le client paye 119,60 €.

Symptômes : montants TTC légèrement sous-évalués, écart visible uniquement par recalcul manuel ou test d'assertion sur la TVA. Logs : [BusinessChaos][A1] TVA incorrecte appliquee: 19.6% au lieu de 20%

A2 — Arrondi prix à l'entier inférieur

Méthode : applyPriceRounding(BigDecimal price, String productName, String userEmail) Compteur : chaos.business.a2.arrondi

Tout produit dont le prix est strictement supérieur à 10 € est arrondi à l'entier inférieur (RoundingMode.FLOOR, scale 0). Un produit affiché à 99,99 € est facturé 99,00 €. La perte par article peut atteindre 0,99 €, soit ≈ 1 % de marge par produit.

Symptômes : écart prix catalogue vs prix panier, manque à gagner silencieux. Logs : [BusinessChaos][A2] Arrondi prix plancher: 99.99 -> 99.00 EUR (perte: 0.99 EUR)

A3 — Stock non décrémenté

Méthode : shouldDecrementStock(String productName, int quantity, String userEmail) Compteur : chaos.business.a3.stock

Après une commande validée, la méthode retourne false — le stock du produit n'est jamais mis à jour en base. Le produit reste éternellement affiché comme disponible, quelle que soit la quantité commandée.

Symptômes : stock affiché identique avant et après commande, survente possible en théorie illimitée. Logs : [BusinessChaos][A3] Stock NON decremente: '{produit}' x{quantite}

Niveau 2 — Confirmé (A4 – A7)

A4 — Email de confirmation : frais de port absents

Méthode : getEmailConfirmationAmount(BigDecimal totalWithShipping, BigDecimal shippingCost, String orderNumber, String userEmail) Compteur : chaos.business.a4.email

L'email de confirmation envoyé au client affiche un montant excluant les frais de port — le client voit un total inférieur à ce qui sera réellement débité. Le montant en base reste correct.

Symptômes : écart entre l'email reçu et le débit bancaire, plaintes clients sur le « surcoût caché ». Détection : test d'assertion comparant le total du JSON commande avec le montant de l'email.

A5 — Double commande sur double-clic

Méthode : isOrderDuplicate(String userEmail, String cartHash) Compteur : chaos.business.a5.doublon

La protection idempotence par (userEmail, cartHash) est désactivée — un double-clic sur le bouton « Commander » crée deux commandes identiques. Au niveau 0, la méthode retourne true si une commande identique a été créée dans les 3 dernières secondes ; au niveau 2+, elle retourne toujours false.

Symptômes : deux commandes consécutives à quelques millisecondes d'écart pour le même utilisateur avec le même hash de panier.

A6 — Code promo invalide accepté silencieusement

Méthode : validatePromoCode(String code, String userEmail) Compteur : chaos.business.a6.promo

Un code promo inexistant (par exemple BLABLA42) est accepté avec une réduction de 0 % et un message neutre « Code promo applique ». L'API retourne valid: true au lieu de valid: false. Le client croit avoir appliqué une promotion alors qu'aucune réduction n'est effective.

Symptômes : message « code appliqué » affiché côté frontend, mais total inchangé. Détection : assertion sur le delta de prix avant/après application du code.

A7 — Délai de livraison en jours calendaires

Méthode : getDeliveryDelay(String shippingMethod, String userEmail) Compteur : chaos.business.a7.livraison

Le délai de livraison annoncé est calculé en jours calendaires au lieu de jours ouvrés. Une commande passée vendredi avec « 3 jours » est annoncée pour lundi (week-end inclus dans le décompte). Le workingDays du retour passe de true à false.

Symptômes : dates de livraison promises qui tombent un dimanche ou un jour férié, retards systématiques côté logistique.

Niveau 3 — Expert (A8 – A11)

A8 — Race condition stock

Méthode : checkStockAvailability(Long productId, int stock, int requested, String userEmail) Compteur : chaos.business.a8.race

La vérification du stock ne pose plus de verrou pessimiste sur la ligne produit. Sous charge concurrente, deux commandes simultanées peuvent passer toutes les deux la vérification alors que le stock ne suffit que pour une — c'est la survente. À faible charge, l'anomalie est invisible.

Logs : [BusinessChaos][A8] Stock verifie SANS VERROU — productId={id} stock={s} demande={q} (sévérité ERROR). Détection : test de charge JMeter avec 50 threads en parallèle sur le même produit dont le stock = 1.

A9 — Injection de log (log poisoning)

Méthode : processInputField(String value, String fieldName, String userEmail) Compteur : chaos.business.a9.inject

Au niveau < 3, les caractères de contrôle (\n, \r, \t) sont échappés. Au niveau 3+, la valeur est insérée brute dans les logs, ce qui permet à un attaquant d'injecter de faux logs (log poisoning) ou des expressions JNDI (pattern ${…}) — à l'origine de Log4Shell.

Symptômes : logs Loki contenant des sauts de ligne fabriqués par le client, fausses entrées qui semblent provenir d'une autre source.

A10 — Total historique commandes faux

Méthode : computeOrderHistoryTotal(List<BigDecimal> orders, String userEmail) Compteur : chaos.business.a10.total

Le total cumulé de l'historique des commandes affiché à l'utilisateur est calculé avec un arrondi par ligne (FLOOR scale 0) au lieu d'une somme exacte. Sur 50 commandes, l'écart peut atteindre plusieurs dizaines d'euros — toujours en défaveur du client.

Logs : [BusinessChaos][A10] Total historique faux: affiche={fake} reel={real} ecart={delta}. Affichage côté UI ≠ valeur DB.

A11 — Token session non invalidé après logout

Méthode : getTokenGracePeriodMs(String userEmail) Compteur : chaos.business.a11.token

Après un logout, l'ancien token de session reste valide pendant 30 secondes — la méthode retourne 30_000L au lieu de 0L. Une attaque replay est possible : un attaquant qui capture le token avant le logout peut continuer à appeler les APIs au nom de l'utilisateur pendant 30 s.

Détection : script de test qui se logge, capture le token, déclenche le logout, puis ré-utilise le token sur /api/auth/me — devrait retourner 401, mais retourne 200 au niveau 3+.

Niveau 4 — Master (A12 – A16)

A12 — Remise fidélité silencieuse

Méthode : applyLoyaltyDiscount(BigDecimal total, long orderCount, String userEmail) Compteur : chaos.business.a12.loyalty

Au niveau < 4, un client avec plus de 5 commandes reçoit automatiquement 5 % de réduction. Au niveau 4, la réduction n'est plus appliquée mais aucune erreur n'est retournée — le client perd silencieusement sa fidélité.

Logs : [BusinessChaos][A12] Remise fidélité non accordée: {n} commandes > 5. Détection : test d'assertion comparant le total calculé au total attendu pour un client fidèle.

A13 — Devise d'affichage incorrecte

Méthode : getCurrency(String context) Compteur : chaos.business.a13.currency

Le champ currency retourné par les APIs produit passe de EUR à USD au niveau 4, sans changer les prix. Un produit affiché 99,99 € est soudainement étiqueté 99.99 USD — soit ≈ 10 % d'écart de valeur réelle selon le taux de change.

Symptômes : devise affichée incohérente avec la zone géographique du client, plaintes immédiates sur les forums clients.

A14 — Frais de port doublés

Méthode : applyShippingCost(BigDecimal shippingCost, String orderNumber, String userEmail) Compteur : chaos.business.a14.shipping

Les frais de port sont multipliés par 2 silencieusement avant intégration au total. Un produit avec 5 € de frais de port se retrouve facturé avec 10 € de port — surcharge invisible côté client jusqu'à réception du débit.

Logs : [BusinessChaos][A14] Frais de port doublés: 5.00 -> 10.00.

A15 — Corruption silencieuse historique (× 1.1)

Méthode : applyHistoryCorruption(BigDecimal amount, Object orderId, String userEmail) Compteur : chaos.business.a15.history

L'API GET /api/orders retourne les totaux multipliés par 1.1 par rapport aux valeurs réellement stockées en base. La modification est en lecture seule — la base reste cohérente, mais l'utilisateur voit une historique falsifiée. Cumulée avec A10, l'historique devient totalement incohérent.

Détection : comparaison ligne à ligne entre les totaux affichés par l'API et le résultat d'un SELECT total FROM orders direct.

A16 — Annulation sans recréditage stock

Méthode : shouldRestockOnCancel(String orderNumber, String userEmail) Compteur : chaos.business.a16.cancel

Quand une commande est annulée (DELETE /api/orders/{id}), le stock n'est pas recrédité. La méthode retourne false. Cumulée avec A3 (stock non décrémenté), elle produit un état incohérent dans les deux sens : le stock dérive sans corrélation avec l'activité réelle.

Logs : [BusinessChaos][A16] Annulation commande SANS remboursement stock — commande={orderNumber} (sévérité ERROR).

Anomalies de validation cumulées

BusinessChaosService expose en plus des flags de validation qui modifient le comportement de ValidationService selon le niveau actif :

Méthode flag Niveau Effet
isPostalCodeLenient = 1 Code postal accepté hors format
isPostalCodeInvertedFR = 2 Inverse les chiffres du code postal FR
isPostalCodeFR4Digits ≥ 3 Accepte 4 chiffres au lieu de 5
isCvvLenient = 1 CVV moins strict
isCvv4Digits ≥ 3 Accepte 4 chiffres CVV
isPhoneLenient = 1 Téléphone moins strict
isPhoneRejectMobileFR ≥ 3 Rejette les 06/07
isNameRejectAccents ≥ 2 Rejette noms accentués
isCardRejectFarExpiry ≥ 2 Refuse les cartes valides très loin dans le temps
isBirthDateAcceptFuture ≥ 3 Accepte des dates de naissance futures

Ces flags sont consommés par ValidationService et UserController — ils ne sont pas comptés dans les compteurs A1–A16 mais relèvent du même chaos métier.

Injection SQL via champs profil

BusinessChaosService expose aussi deux méthodes d'injection SQL ciblées sur les champs profil utilisateur — utilisées par UserController pour provoquer des DataTruncation et des erreurs JDBC observables :

Méthode Niveau Comportement
getInjectedPostalCode ≥ 2 Injecte 999999 (6 ch.) ou 9999999999 (10 ch.) > VARCHAR(5)
getInjectedCountry ≥ 3 Injecte FRANCE (6 ch.) > VARCHAR(2)

Ces injections déclenchent des SQLException côté JDBC qui sont loggées via logSqlError() et apparaissent dans le monitoring sous le tag SQL_ERROR.

API — endpoints publics

Endpoint Description
GET /api/chaos/public/business/status Niveau courant + 16 compteurs
GET /api/chaos/public/business/logs Activity log (200 dernières entrées)
GET /api/chaos/public/business/anomalies?level=N Catalogue pédagogique pour le niveau N

API — endpoints admin

curl -X POST https://perfshop-api.perfshop.io/api/admin/chaos/business \
  -H "X-Admin-Token: $TOKEN" -H "Content-Type: application/json" \
  -d '{"level": 4}'

curl -X POST https://perfshop-api.perfshop.io/api/admin/chaos/business/reset \
  -H "X-Admin-Token: $TOKEN"

Activation par l'étudiant

POST /api/chaos/student/business body {"level": N} — exige le mode étudiant et une licence valide pour level > 0. Sans licence, retourne HTTP 402 LICENSE_REQUIRED.