Aller au contenu

PD-180 — Scénarios de tests contractuels (corrigés v3)

1. Références

  • Spécification : PD-180-specification.md
  • Story : PD-180
  • Scope technique : ProbatioVault-backend (NestJS, TypeORM, PostgreSQL, BullMQ)

2. Matrice de couverture

ID Invariant ID Critère ID Test Couverture Commentaire
INV-01 CA-12 TC-INV-01 Oui Payload sans données sensibles
INV-02 CA-03 TC-NOM-05 Oui Signature HMAC présente/valide
INV-03 CA-04 TC-ERR-06 Oui Fenêtre anti-rejeu ±5 min
INV-04 CA-05 TC-ERR-01 / TC-NEG-01 Oui Schéma strict https uniquement
INV-05 CA-06 TC-ERR-07 Oui 3xx échec sans follow
INV-06 CA-08 TC-INV-06 Oui Intention avant tentative
INV-07 CA-08 TC-INV-07 Oui Append-only métier + purge technique
INV-08 CA-11 TC-NOM-02 / TC-ERR-04 Oui Isolation inter-tenant
INV-09 CA-01, CA-10 TC-NOM-01 / TC-NOM-08 Oui Secret non exposé en clair
INV-10 CA-11 TC-INV-10 Oui orgId JWT obligatoire
INV-11 CA-12 TC-NOM-13 Oui Non-mutation probatoire
INV-12 CA-12 TC-INV-12 Oui Causalité émission après journal source
INV-13 CA-01 TC-INV-13 Oui Transitions non listées interdites
INV-14 CA-12 TC-NOM-11 / TC-NOM-12 Oui Crash pré/post commit
INV-15 CA-14 TC-INV-15 Oui SSRF + DNS rebinding bloqués
CA-02 TC-ERR-02 Oui Quota 6e webhook rejeté 409
CA-07 TC-NOM-09 / TC-NR-03 Oui Séquence retry exacte 10 tentatives
CA-13 TC-NOM-10 Oui 100/min/org en sliding window 60s
CA-15 TC-SEC-01 / TC-SEC-02 / TC-NOM-17 Oui CSPRNG >=32 bytes + stockage hash + relecture DB + vérification partenaire
§5.1 variante B TC-NOM-15 Oui Payload non-documentaire (account.device.revoked)
§5.1 variante C TC-NOM-16 Oui Payload ping (webhook.ping)

3. Scénarios de test — Flux nominaux

TEST-ID: TC-NOM-01
Référence spec: INV-09, CA-01

GIVEN
  - Tenant authentifié (JWT orgId=A)
  - 0 webhook existant
WHEN
  - Création d’un webhook HTTPS valide
THEN
  - Réponse succès et webhook créé pour org A
  - Secret visible une seule fois, jamais relisible en clair
AND
  - Les réponses ultérieures n’exposent qu’un masque (4 derniers caractères)
TEST-ID: TC-NOM-02
Référence spec: INV-08, CA-01, CA-11

GIVEN
  - Deux organisations A et B
  - Webhooks existants dans A et B
WHEN
  - Liste/lecture avec JWT orgId=A
THEN
  - Seules les ressources de A sont visibles
AND
  - Aucune fuite de données de B
TEST-ID: TC-NOM-03
Référence spec: Flux B, INV-13, CA-01

GIVEN
  - Webhook en état ACTIVE
WHEN
  - Mise à jour vers INACTIVE puis retour vers ACTIVE
THEN
  - Transitions autorisées appliquées immédiatement
AND
  - Les livraisons futures respectent l’état courant
TEST-ID: TC-NOM-04
Référence spec: Flux C, INV-13

GIVEN
  - Webhook ACTIVE ou INACTIVE
WHEN
  - Suppression webhook
THEN
  - État devient DELETED
AND
  - Toute transition ultérieure depuis DELETED est refusée (terminal)
TEST-ID: TC-NOM-05
Référence spec: INV-02, CA-03

GIVEN
  - Événement source journalisé et webhook actif
WHEN
  - Tentative de livraison est émise
THEN
  - Header X-ProbatioVault-Signature respecte t=<unix>,v1=<hex64>
  - v1 = HMAC-SHA256(signing_key, <t>.<JSON.stringify(payload)>) où signing_key = secret_sha256 (hash stocké en DB)
AND
  - Tentative journalisée avec code/résultat, durée, numéro de tentative
TEST-ID: TC-NOM-06
Référence spec: Flux E

GIVEN
  - Webhook existant
WHEN
  - Déclenchement ping
THEN
  - webhook.ping suit la variante C du schéma whitelist (data.user_id + data.metadata, sans doc_id/hash)
AND
  - Signature, retry, timeout (5s), SSRF checks et journalisation identiques au flux métier
TEST-ID: TC-NOM-07
Référence spec: Flux F, CA-09

GIVEN
  - Événement historique autorisé
WHEN
  - Replay déclenché
THEN
  - Nouveau event_id produit
  - timestamp renouvelé
AND
  - original_event_id est consultable via journal interne/API de corrélation
TEST-ID: TC-NOM-08
Référence spec: Flux G, CA-10, CA-15

GIVEN
  - Webhook avec secret S1
WHEN
  - Rotation secret vers S2
THEN
  - S2 actif immédiatement et S1 invalide immédiatement
AND
  - Chaque tentative post-rotation relit le hash secret en DB (aucun cache worker)
TEST-ID: TC-NOM-09
Référence spec: CA-07

GIVEN
  - Endpoint partenaire configurable par code HTTP
WHEN
  - Réponse 204 puis 500 puis 401
THEN
  - 204 => DELIVERED
  - 500/401 => échec retriable
AND
  - La séquence retry suit exactement 1m,5m,15m,1h,1h,1h,1h,1h,1h,1h (max 10 tentatives)
TEST-ID: TC-NOM-10
Référence spec: CA-13

GIVEN
  - Organisation A avec 1 webhook actif abonné au même event_type
WHEN
  - 120 événements produits en 60 secondes glissantes (= 120 intentions de livraison)
THEN
  - Les 100 premières intentions passent immédiatement
  - Les 20 excédentaires sont mis en attente dans la file BullMQ
AND
  - Aucune intention n’est perdue et la file se résorbe dans l’ordre d’arrivée
TEST-ID: TC-NOM-11
Référence spec: INV-14 (crash pré-commit)

GIVEN
  - Injection d’un crash avant commit transactionnel
WHEN
  - Tentative d’émission webhook
THEN
  - Rollback complet
AND
  - Aucun artefact persistant (intention/tentative) ni envoi effectif
TEST-ID: TC-NOM-12
Référence spec: INV-14 (crash post-commit)

GIVEN
  - Intention commitée
  - Crash avant exécution complète worker
WHEN
  - Redémarrage système
THEN
  - Donnée DB conservée
AND
  - Livraison rattrapée asynchronement sans perte
TEST-ID: TC-NOM-13
Référence spec: INV-11, CA-12

GIVEN
  - Snapshot initial états probatoires
WHEN
  - Exécution CRUD webhook, ping, replay, retry, rotation
THEN
  - Aucun état probatoire ne change
AND
  - Seules entités webhook/livraison/journal évoluent
TEST-ID: TC-NOM-14
Référence spec: CA-08

GIVEN
  - Journal de tentatives de livraison sur période >30 jours
  - Intentions de livraison (registre d’événements) sur période >30 jours
WHEN
  - Consultation API paginée/filtrée
THEN
  - Tentatives <=30 jours consultables, tentatives >30 jours purgées
  - Intentions de livraison conservées sans limite de durée
AND
  - Aucune mutation métier UPDATE/DELETE n’est exposée
TEST-ID: TC-NOM-15
Référence spec: §5.1 variante B, INV-01

GIVEN
  - Événement account.device.revoked émis
  - Webhook actif abonné à cet event_type
WHEN
  - Payload construit et émis
THEN
  - Payload suit variante B : data.device_id (UUID), data.user_id (UUID), data.metadata ({})
  - Pas de champ data.doc_id ni data.hash
AND
  - additionalProperties:false respecté
  - Signature calculée avec ordre de sérialisation variante B
TEST-ID: TC-NOM-16
Référence spec: §5.1 variante C, Flux E

GIVEN
  - Webhook existant
WHEN
  - Ping déclenché
THEN
  - Payload suit variante C : data.user_id (UUID), data.metadata ({})
  - Pas de champ data.doc_id, data.hash ni data.device_id
AND
  - additionalProperties:false respecté
  - Signature calculée avec ordre de sérialisation variante C
TEST-ID: TC-NOM-17
Référence spec: §5.2, §5.3 (clé HMAC = SHA-256(secret_brut))

GIVEN
  - Webhook créé, secret brut S reçu par le partenaire
  - Partenaire calcule K = SHA-256(S)
WHEN
  - Événement livré au partenaire
THEN
  - Partenaire vérifie HMAC-SHA256(K, t + "." + body) == v1 du header
AND
  - La vérification réussit (preuve que signing_key = secret_sha256 = SHA-256(S))

4. Scénarios de test — Cas d’erreur

TEST-ID: TC-ERR-01
Référence spec: ERR-01, INV-04, CA-05

GIVEN
  - Tenant authentifié
WHEN
  - target_url en http://, ftp://, file://, data:, javascript:
THEN
  - Rejet 400
AND
  - Aucune création/mise à jour persistée
TEST-ID: TC-ERR-02
Référence spec: ERR-02, CA-02

GIVEN
  - 5 webhooks existants pour org A
WHEN
  - Création d’un 6e webhook
THEN
  - Rejet 409 Conflict
AND
  - Nombre de webhooks inchangé
TEST-ID: TC-ERR-03
Référence spec: ERR-03

GIVEN
  - Tenant authentifié
WHEN
  - event_type hors enum contractuelle
THEN
  - Rejet 400
AND
  - Aucun abonnement invalide persistant
TEST-ID: TC-ERR-04
Référence spec: ERR-04, INV-08

GIVEN
  - JWT orgId=A et ressource org B
WHEN
  - Lecture/modification/suppression ressource B
THEN
  - Rejet 403 systématique
AND
  - Aucune fuite cross-tenant
TEST-ID: TC-ERR-05
Référence spec: ERR-05

GIVEN
  - Endpoint partenaire retourne 401 (ex. signature non reconnue côté partenaire)
WHEN
  - Tentative de livraison est effectuée
THEN
  - Tentative classée en échec côté émetteur
AND
  - Retry planifié selon politique
TEST-ID: TC-ERR-06
Référence spec: ERR-06, INV-03

GIVEN
  - Endpoint partenaire retourne 401 (timestamp hors fenêtre selon sa validation)
WHEN
  - Tentative de livraison est effectuée
THEN
  - Tentative classée en échec côté émetteur
AND
  - Retry planifié selon politique
TEST-ID: TC-ERR-07
Référence spec: ERR-07, INV-05

GIVEN
  - Endpoint cible renvoie 301/302/307/308
WHEN
  - Livraison exécutée
THEN
  - Tentative = échec
  - Aucune redirection suivie
AND
  - Retry planifié si tentatives restantes
TEST-ID: TC-ERR-08
Référence spec: ERR-08

GIVEN
  - Endpoint cible ne répond pas dans les 5 secondes
WHEN
  - Livraison exécutée
THEN
  - Tentative = échec timeout après exactement 5 secondes
AND
  - Retry planifié selon séquence contractuelle
TEST-ID: TC-ERR-09
Référence spec: ERR-09, CA-07

GIVEN
  - Échecs répétés de livraison
WHEN
  - 10e tentative échoue
THEN
  - État final = FAILED
AND
  - Aucune tentative supplémentaire
TEST-ID: TC-ERR-10
Référence spec: ERR-10, CA-10, CA-15

GIVEN
  - Événements en file d’attente
  - Rotation secret réalisée avant exécution
WHEN
  - Worker envoie les événements
THEN
  - Signature calculée avec nouveau hash secret relu en DB
AND
  - Vérification avec ancien secret échoue
TEST-ID: TC-ERR-11
Référence spec: ERR-11, Flux F

GIVEN
  - Cas A : événement introuvable ou purgé (>30j)
  - Cas B : événement d'une autre organisation
  - Cas C : événement en état PENDING/IN_PROGRESS/RETRY_SCHEDULED
WHEN
  - Appel replay pour chaque cas
THEN
  - Cas A : rejet 404
  - Cas B : rejet 403
  - Cas C : rejet 409 Conflict
AND
  - Aucun nouvel événement de notification créé dans aucun cas
TEST-ID: TC-ERR-12
Référence spec: ERR-12

GIVEN
  - Données source internes incohérentes (ex : doc_id manquant pour événement documentaire, hash non hex64)
WHEN
  - Le système tente de construire le payload webhook
THEN
  - L'intention de livraison est créée en état FAILED avec motif PAYLOAD_VALIDATION_ERROR
  - Aucune tentative HTTP n'est émise
AND
  - L'erreur est loggée pour investigation (corruption données internes)

5. Tests d’invariants dédiés (GWT manquants ajoutés)

TEST-ID: TC-INV-01
Invariant: INV-01

GIVEN
  - Un événement webhook prêt à émettre
WHEN
  - Inspection du payload contractuel
THEN
  - Seuls les champs whitelistés sont présents
AND
  - Aucun secret, blob chiffré ou contenu sensible n’est présent
TEST-ID: TC-INV-06
Invariant: INV-06

GIVEN
  - Un événement source journalisé
WHEN
  - L’émetteur lance la livraison
THEN
  - Une intention de livraison existe déjà en DB avec horodatage antérieur à la 1re tentative
AND
  - L’ordre temporel intention -> tentative est strictement respecté
TEST-ID: TC-INV-07
Invariant: INV-07

GIVEN
  - Des entrées de journal de tentatives existent
WHEN
  - Tentative de mutation métier UPDATE/DELETE via API ou service
THEN
  - Opération refusée
AND
  - La purge technique >30 jours opérée par cron/TTL reste autorisée
TEST-ID: TC-INV-10
Invariant: INV-10

GIVEN
  - JWT orgId=A
  - Paramètre client forgé orgId=B
WHEN
  - Appel API webhook
THEN
  - Scope effectif = A
AND
  - Aucun accès à B n’est possible
TEST-ID: TC-INV-12
Invariant: INV-12

GIVEN
  - Un événement non journalisé dans le module source
WHEN
  - Tentative d’émission webhook
THEN
  - Rejet de l’émission
AND
  - Aucune intention de livraison créée
TEST-ID: TC-INV-13
Invariant: INV-13

GIVEN
  - Ressource webhook en DELETED
  - Livraison en DELIVERED ou FAILED
WHEN
  - Tentative de transition non listée
THEN
  - Transition refusée explicitement
AND
  - État terminal inchangé
TEST-ID: TC-INV-15
Invariant: INV-15

GIVEN
  - target_url résout vers IP privée/loopback/link-local/metadata cloud
WHEN
  - Validation à la création OU juste avant envoi
THEN
  - Rejet/blocage de l’envoi
AND
  - Si DNS rebinding est détecté entre résolutions, envoi refusé

6. Tests sécurité secrets et signature

TEST-ID: TC-SEC-01
Référence spec: CA-15

GIVEN
  - Création webhook
WHEN
  - Secret généré
THEN
  - Taille minimale 32 bytes (source CSPRNG)
AND
  - DB ne contient que le hash SHA-256 (hex64), jamais le secret brut
TEST-ID: TC-SEC-02
Référence spec: CA-15

GIVEN
  - Une livraison avec retries
WHEN
  - Rotation secret entre deux tentatives
THEN
  - La tentative suivante relit le hash en DB
AND
  - Signature utilise le secret/hash courant, pas une valeur cache
TEST-ID: TC-SEC-03
Référence spec: §5.2 (canonicalisation JSON)

GIVEN
  - Même payload logique construit avec même ordre d’insertion
WHEN
  - Signature calculée deux fois
THEN
  - JSON.stringify produit exactement la même chaîne
AND
  - v1 est identique (à t identique)

7. Tests de non-régression

Test ID Objet Observable
TC-NR-01 Couverture 7 event_types v1 + webhook.ping Tous émettent payload whitelist + signature valide
TC-NR-02 Quota 5 webhooks/org 6e rejeté 409
TC-NR-03 Retry exact 10 tentatives Séquence 1m,5m,15m,1h,1h,1h,1h,1h,1h,1h inchangée
TC-NR-04 RLS inter-tenant Aucun accès croisé
TC-NR-05 Rotation secret Ancien secret invalide immédiatement
TC-NR-06 Append-only métier Aucune mutation métier du journal
TC-NR-07 Non-mutation probatoire États probatoires inchangés
TC-NR-08 SSRF protection Ranges interdits + rebinding toujours bloqués
TC-NR-09 Règle HTTP de succès Seuls 2xx donnent DELIVERED

8. Observabilité requise

  • États webhook/livraison lisibles via API : ACTIVE, INACTIVE, DELETED, PENDING, IN_PROGRESS, RETRY_SCHEDULED, DELIVERED, FAILED.
  • Journal de tentatives append-only métier : tentative n°, durée, code/résultat, horodatage, corrélation event_id.
  • Capture HTTP sortante : URL finale appelée, headers (dont X-ProbatioVault-Signature), payload exact signé.
  • Traces sécurité : résultat validation SSRF, DNS résolu (A/AAAA), motif de blocage.
  • Traces secret : preuve de relecture DB par tentative (sans exposer valeur secrète).

9. Règles non testables

Aucune.
Le contrat est entièrement testable dans cette version.


10. Verdict QA

  • ✅ Testable complètement (sans réserve)

Justification : toutes les ambiguïtés de format, sécurité, retry, SSRF, états et codes HTTP ont été fermées contractuellement ; la couverture de tests inclut invariants, nominaux, erreurs, adversarial et non-régression.