8 Commits

Author SHA1 Message Date
ordinarthur
5b8898ba9c test(auth): enrichit l'assertion signup duplicate email (format de réponse)
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Le test "refuse un email déjà pris" vérifiait seulement `assertStatus(422)`.
On enrichit pour vérifier la shape complète :
  - body.errors est un array non-vide
  - au moins une entrée a `field === 'email'`
  - cette entrée a un code et un message strings

Pourquoi : un mutation test du 2026-05-18 a révélé qu'un simple
`assertStatus(422)` laissait passer le retrait du `.unique()` côté
validator Vine — la contrainte DB UNIQUE(email) prenait le relais
silencieusement via le handler PG 23505, qui renvoie le même 422.

Apprentissage : .unique() Vine et DB unique sont équivalents côté
contrat API (le handler `23505` met `field='email'` identique). Le
.unique() Vine est donc une optimisation (skip round-trip DB) plutôt
qu'une garantie de sécurité. On garde la double protection comme
best practice (defense-in-depth).

Le test enrichi ne catche pas le retrait du .unique() spécifiquement
(les 2 paths sont indistinguables côté client) MAIS catche des
régressions plus graves :
  - Si le handler 23505 casse → 500 au lieu de 422
  - Si `field` n'est plus posé → SPA ne peut plus highlight l'input
  - Si la shape {errors:[...]} change → contrat API cassé

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:04:08 +02:00
ordinarthur
59f81879d8 test(e2e): tests Playwright multi-stack — vrai navigateur, DB isolée, Stripe mocké
All checks were successful
Build & Deploy Landing / build-and-deploy (push) Successful in 1m39s
Build & Deploy API / build-and-deploy (push) Successful in 2m30s
Build & Deploy Web / build-and-deploy (push) Successful in 1m21s
Ajoute une couche end-to-end où un Chromium drive la SPA + API ensemble
contre une DB Postgres séparée, avec Stripe entièrement mocké au niveau
API. 6 scénarios couverts (signup + onboarding + 4 sur le billing trial).

Architecture :
  - DB `rubis_test_e2e` séparée, TRUNCATE entre tests (~50 ms reset)
  - Routes test-only `/__test__/*` gated par NODE_ENV=test_e2e
    (reset, install Stripe mock, fire webhook, lire org state, last-org)
  - Stripe mocké via __setStripeForTests — pas d'appel réseau
  - Playwright spawn API + SPA automatiquement (webServer config)
  - CORS étendu à test_e2e pour le cross-origin localhost:5173 → :3333

Scénarios :
  - signup.spec.ts : signup → onboarding 3 étapes → dashboard (assert rubis hero)
  - billing-trial.spec.ts :
      • démarrer essai 14j → redirect Stripe Checkout (mock)
      • fallback Free 2 factures continue l'onboarding
      • webhook checkout.completed → org en trialing + trial_ends_at
      • retour ?trial=cancel après abandon
      • inspection DB : stripeCustomerId posé après start-trial

Scripts :
  - pnpm e2e          (headless)
  - pnpm e2e:headed   (Chromium visible)
  - pnpm e2e:ui       (mode interactif Playwright)
  - pnpm e2e:setup    (crée + migre rubis_test_e2e via docker exec)

Documentation : docs/tech/e2e-tests.md — architecture, scénarios,
extensions, CI, troubleshooting.

Limites assumées :
  - L'UI Stripe Checkout (3DS, formulaire CB) n'est pas testée — externe.
    Pour ça : playbook manuel docs/tech/stripe-trial-e2e-playbook.md.
  - Le rendu du banner "Essai Pro" n'est pas asserté en E2E à cause de
    TanStack Query staleTime — couvert par les tests vitest à la place.

État global du chantier billing : 127 tests japa + 6 Playwright + 11
vitest = couverture multi-niveaux.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:58:51 +02:00
ordinarthur
0db7ff877c fix(clients): aligne adresse structurée sur convention Lucid snakeCase
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m17s
La migration 1778800000100_enrich_clients_for_invoicing avait créé les
colonnes `address_line1` et `address_line2` (sans underscore avant le
chiffre), mais Lucid v22 auto-snake-case `addressLine1` → `address_line_1`
côté écriture. Résultat : tous les INSERT dans `clients` cassaient avec
`column "address_line_1" of relation "clients" does not exist`.

Bug latent — surfacé en lançant la suite functional complète après
`node ace migration:run`. Affectait 20 tests Clients/Dashboard/Invoices.

Fix : migration de rename qui aligne `address_line1` → `address_line_1`
et `address_line2` → `address_line_2`. Le `RENAME COLUMN` préserve les
données existantes en prod. Modèle Client simplifié (les déclarations
manuelles ne sont plus nécessaires depuis que `schema.ts` les a régen).

Bonus : fix du test `invoices.spec.ts` "liste paginée + meta total/page"
qui créait 3 factures, ce qui dépasse la nouvelle limite Free 2 (ADR-023).
Posé un `gracePeriodEndsAt` futur sur l'org du test pour bypass le quota.

État après ce commit : 127 tests verts (60 unit + 67 functional).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:10:49 +02:00
ordinarthur
094c26059f test(billing): tests E2E HTTP du tunnel essai 14 j + playbook Stripe test mode
All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m18s
Ajoute 16 tests E2E qui hit les vraies routes `/api/v1/billing/*` à
travers le middleware auth, les validators et la persistance DB.
Complémentaire des 60 tests unitaires sur les services.

Suites couvertes :
  - POST /start-trial : 200 happy path, customer Stripe réutilisé,
    409 trial déjà consommé (2 garde-fous), 401 sans Bearer
  - GET  /subscription : expose inTrial + trialEndsAt, garde-fou
    trial_ends_at passé
  - POST /webhook : checkout.completed, subscription.updated trialing→active,
    trial_will_end → enqueue recap (avec spy), payment_failed → past_due,
    subscription.deleted → free + trial_ends_at conservé
  - Idempotence : 2× le même event = même état final
  - Event type inconnu → 200 silencieux (pas de DB write)
  - 400 si stripe-signature absent / signature invalide

Helpers de test :
  - installFullStripeMock(opts) → mock complet : customers, prices,
    checkout, billingPortal, subscriptions, webhooks. Avec
    passThroughWebhook qui bypass la vérif signature pour tester
    le routing applicatif sans signer manuellement chaque payload.

env.test : STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET dummy +
WEB_URL/LANDING_URL.

Documentation : docs/tech/stripe-trial-e2e-playbook.md — playbook
manuel pour valider en mode Stripe test (5 scénarios : happy path
3DS, carte refusée au prélèvement, annulation Customer Portal,
re-trial bloqué, fallback Free). Utilise Stripe Test Clocks pour
fast-forward sans attendre 14 jours réels.

Total après ce commit : 76 tests sur la chaîne billing (60 unit + 16 E2E).
Les cas Stripe-side (3DS UI réel, prélèvement effectif J+14) restent
à valider manuellement via le playbook avant le go-live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 14:01:16 +02:00
ordinarthur
5e41e2a9fa add ocr + add factures 2026-05-06 18:47:35 +02:00
ordinarthur
554ae4ba4a test(api): tests fonctionnels Invoices + Imports + Dashboard
invoices.spec.ts (10 cas) :
- Création : 201 + rubisEarned=1 (bonus saisie) + status=pending
- Création client à la volée : si nom non matché + email fourni → client créé
- 422 client_email_required si pas d'email pour création à la volée
- Si planId fourni : RelanceTasks scheduled créées (assertion sur DB count)
- Numéro unique par org : 422 sur duplicate (testant l'exception handler)
- mark-paid idempotent : 2e appel ne re-bumpe pas rubisEarned ni org.rubis_count
- mark-paid annule les RelanceTasks scheduled (passe à cancelled)
- Cross-org : user B → 404 sur mark-paid d'une facture de A
- GET /invoices : pagination + meta total/page
- GET /invoices/counts : agrège par status

imports.spec.ts (6 cas) :
- POST /upload mock JSON : 1 batch + N drafts en pending, mock OCR rempli les fields
- Refus > 20 fichiers
- Validate transforme draft en Invoice + status=validated + invoiceId set
- Validate sur draft déjà processed → 409 draft_already_processed
- Skip → status=skipped, idempotent
- DELETE batch → CASCADE supprime les drafts

dashboard.spec.ts (6 cas) :
- KPIs zéros sur org vierge
- factureToRelance compte les invoices pending
- Après mark-paid : encaisseCents et rubisCount bumpent (org.rubisCount agrégé)
- Activity vide sur org sans actions
- Activity loggue invoice_paid après mark-paid (label dans le feed)
- Top-late liste les clients avec invoices actives en retard (dueDate < today)
2026-05-06 15:53:14 +02:00
ordinarthur
691b5fd09f test(api): tests fonctionnels Clients + Plans (CRUD + cross-org + validation)
Helper response.ts : `body<T>()` pour caster Tuyau strict response shapes (Tuyau type chaque code de statut comme une union, assertStatus ne narrow pas → on cast explicitement vers ApiOk<T>/ApiError/ApiConflict<T>).

clients.spec.ts (16 cas) :
- POST /clients : refus sans email (422 + field=email), refus SIRET ≠ 14 chiffres, création OK avec UUID + association org, doublon nom case-insensitive (409 + payload existing)
- GET /clients : isolation cross-org (user A ne voit pas les clients de B), withStats=1 enrichit (zéros sans factures), recherche q ILIKE
- Perms cross-org : user B → 404 sur GET/PATCH d'un client de A, l'objet ne bouge pas

plans.spec.ts (7 cas) :
- GET /plans : 4 plans pré-fournis avec steps préchargés, isolation cross-org (UUIDs disjoints entre A et B)
- GET /plans/:slug : steps ordonnés, 404 si inconnu
- PATCH /plans/:slug : remplace les steps en bloc dans une tx, rejette tone invalide, cross-org (B édite SA copie sans toucher celle de A)
2026-05-06 15:51:03 +02:00
ordinarthur
fc66d80f56 test(api): setup Japa + tests fonctionnels auth (signup/login/logout/onboarding)
Setup :
- .env.test étoffé : DRIVE_DISK=fs, MAIL_DRIVER=smtp local, OCR_PROVIDER=mock. Réutilise la DB rubis_dev avec global transactions par test (rollback auto, isolation parfaite).
- Schedulers (relance + checkin) détectent NODE_ENV=test et skippent BullMQ.add. Les tasks DB sont quand même créées (utiles pour assertions) mais aucun job orphelin n'arrive en Redis après rollback de tx.
- helpers/auth.ts : factory createTestUser() qui crée org + user + 4 plans pré-fournis dans une tx, retourne user/org/accessToken/bearer header. createTwoOrgs() pour les tests cross-org à venir.

Tests fonctionnels auth (tests/functional/auth.spec.ts) :
- Signup : crée user + org + 4 plans pré-fournis (vérifie les slugs), refuse email mal formé / password court / email déjà pris
- Login : émet AuthSession avec credentials valides, rejette mauvais password / email inconnu
- Bearer auth : 401 sans token, 401 avec token bidon, 200 avec token valide
- Logout : révoque le token courant, requêtes suivantes en 401
- Onboarding : PATCH /organizations/me pose onboardingCompletedAt à la 1re mise du nom, idempotent ensuite

Pour lancer : `pnpm -F api test`
2026-05-06 15:45:11 +02:00