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.