chore(api): retire l'intégration Cucumber/Gherkin
Retire la suite BDD Gherkin (apps/api/features/) — pas cassée, mais mise de côté pour l'instant en même temps que l'intégration côté Cypress (voir PR #23, fermée). À reprendre plus tard, probablement avec une approche différente des deux côtés. Vérifié avant retrait que la suite Mocha (apps/api/test/) couvre déjà les mêmes scénarios, domaine par domaine (santé, référentiels, authentification, profil, foyer, planning, recettes, préférences) — souvent avec plus de détail (ex. reference.feature ne couvre pas /reference/ingredients, contrairement à test/reference.test.ts). Pas de perte de couverture réelle. - apps/api/features/ (scénarios .feature + step-definitions + world/hooks) et cucumber.cjs supprimés - apps/api/package.json : retire le script `test:bdd` et la dépendance @cucumber/cucumber (@faker-js/faker conservé — aussi utilisé par la suite Mocha) - .github/workflows/ci.yml : retire l'étape `pnpm --filter api test:bdd` - README : retire toute mention Cucumber/Gherkin/BDD (section dédiée, commandes, description du monorepo, note faker.js) Le côté Cypress était déjà propre sur main (l'intégration testée sur la branche feat/cypress-cucumber n'a jamais été mergée — PR fermée, branche supprimée).
This commit is contained in:
parent
c63bc15ea4
commit
ffb1e06130
23 changed files with 16 additions and 1451 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -64,7 +64,6 @@ jobs:
|
|||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm --filter api exec prisma migrate deploy
|
||||
- run: pnpm --filter api test
|
||||
- run: pnpm --filter api test:bdd
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
51
README.md
51
README.md
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Monorepo pnpm workspaces :
|
||||
|
||||
- `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha + Cucumber/BDD)
|
||||
- `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha)
|
||||
- `apps/web` — frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard.
|
||||
Page de connexion/inscription en place ; le reste est encore un squelette générique.
|
||||
- `packages/shared` — code partagé entre `api` et `web` : schémas zod (`signupSchema`,
|
||||
|
|
@ -108,38 +108,12 @@ pnpm dev:web
|
|||
pnpm lint # Biome (lint + format check)
|
||||
pnpm lint:fix # Biome --write
|
||||
pnpm test # tests unitaires/intégration (Mocha, apps/api)
|
||||
pnpm --filter api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, apps/api)
|
||||
pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
|
||||
pnpm build # build de tous les workspaces
|
||||
```
|
||||
|
||||
La CI GitHub Actions (`.github/workflows/ci.yml`) exécute quatre jobs indépendants (`lint`, `test`, `build`, `e2e`) en parallèle, sur chaque push (toutes branches) et sur chaque PR vers `main` — pas de chaînage entre eux, chacun apparaît comme son propre check. Voir aussi [Déploiement](#déploiement) pour le pipeline de release (`.github/workflows/release.yml`).
|
||||
|
||||
### Cucumber (apps/api)
|
||||
|
||||
Tests d'intégration lisibles en Gherkin, en complément de Mocha (qui reste pour les
|
||||
tests unitaires purs) :
|
||||
|
||||
- `apps/api/features/*.feature` — scénarios en Given/When/Then (`health.feature` sert
|
||||
d'exemple)
|
||||
- `apps/api/features/step-definitions/*.steps.ts` — implémentation des steps
|
||||
- `apps/api/features/support/world.ts` — contexte partagé entre les steps d'un
|
||||
scénario (instancie l'app Express in-process via `createApp()`, comme le fait déjà
|
||||
supertest côté Mocha — pas besoin de lancer un vrai serveur)
|
||||
- `apps/api/cucumber.cjs` — config (extension `.cjs` volontaire, voir la remarque
|
||||
TypeScript/ESM ci-dessous)
|
||||
|
||||
Pour ajouter un scénario : écrire le `.feature`, lancer `pnpm --filter api test:bdd`,
|
||||
implémenter les steps manquants (Cucumber affiche des snippets tout prêts pour ceux
|
||||
qui n'existent pas encore).
|
||||
|
||||
> **Piège TypeScript/ESM à connaître** (déjà rencontré avec `cypress.config.ts`) :
|
||||
> les fichiers de config d'outils tiers qui font du chargement dynamique de TS
|
||||
> (`cucumber.cjs`, `cypress.config.ts`…) sont sensibles au `"type": "module"` du
|
||||
> `package.json`. `cucumber.cjs` évite le problème *pour sa propre config* en étant
|
||||
> explicitement CommonJS ; les steps/world restent en `.ts` ESM classique et sont
|
||||
> chargés via `tsx` (`NODE_OPTIONS=--import=tsx`, voir le script `test:bdd`).
|
||||
|
||||
## Déploiement
|
||||
|
||||
Une seule image Docker (`apps/api/Dockerfile`) sert à la fois l'API et le frontend
|
||||
|
|
@ -204,14 +178,14 @@ encore implémenté).
|
|||
> concrètement (`argon2.hash(...)` dans un `node -e`) avant de merger, un `pnpm build`
|
||||
> qui passe ne suffit pas à détecter un crash runtime.
|
||||
|
||||
Les tests (Mocha + Cucumber) tournent avec un coût argon2 réduit
|
||||
Les tests (Mocha) tournent avec un coût argon2 réduit
|
||||
(`NODE_ENV=test`, voir `auth.service.ts`) — le coût par défaut est volontairement
|
||||
élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI
|
||||
provisionne un vrai Postgres de service (`.github/workflows/ci.yml`) et exécute
|
||||
`prisma migrate deploy` avant les tests.
|
||||
|
||||
> **Les tests automatisés et `pnpm dev:api` partagent la même base Postgres locale.**
|
||||
> Lancer `pnpm test`/`test:bdd` **vide `user_profiles`/`house`** (`TRUNCATE ... CASCADE`,
|
||||
> Lancer `pnpm test` **vide `user_profiles`/`house`** (`TRUNCATE ... CASCADE`,
|
||||
> voir `test-support/reset-db.ts`) — si tu es en train de tester manuellement à la main
|
||||
> (via le navigateur ou curl) contre le serveur de dev, un run de tests en parallèle
|
||||
> efface tes données de test sans prévenir. Pas un bug, juste à savoir.
|
||||
|
|
@ -371,12 +345,10 @@ page fetch son propre profil frais (`apiClient.me()`) au montage, et
|
|||
`AuthContext.refreshUser()` (nouveau) est appelé après une sauvegarde réussie du
|
||||
régime pour que le reste de l'app reste cohérent aussi.
|
||||
|
||||
Tests Cypress (`apps/web/cypress/e2e/`) : `smoke.cy.ts` + `auth.cy.ts` +
|
||||
`home-planning.cy.ts` + `onboarding.cy.ts` + `household.cy.ts` mockent l'API via
|
||||
`cy.intercept` plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne
|
||||
provisionne pas de Postgres/API, seulement le serveur de dev Vite. Le comportement
|
||||
réel de l'API est couvert par les suites Mocha/Cucumber d'`apps/api` (contre une
|
||||
vraie base).
|
||||
Tests Cypress (`apps/web/cypress/e2e/*.cy.ts`) : mockent l'API via `cy.intercept`
|
||||
plutôt que de dépendre d'un vrai backend — le job e2e de la CI ne provisionne pas de
|
||||
Postgres/API, seulement le serveur de dev Vite. Le comportement réel de l'API est
|
||||
couvert par la suite Mocha d'`apps/api` (contre une vraie base).
|
||||
|
||||
> **Cypress ne peut pas tourner en local dans un environnement Windows sandboxé** :
|
||||
> Chromium/Electron headless plante au lancement du process GPU
|
||||
|
|
@ -425,10 +397,5 @@ en ajouter une est une question de fichier de locale, pas de code. Détail :
|
|||
## Données de test (faker.js)
|
||||
|
||||
`apps/api` utilise [`@faker-js/faker`](https://fakerjs.dev/) pour toutes les données
|
||||
de test dans `test/auth.test.ts` (Mocha) et le "bruit" (prénom/nom de remplissage)
|
||||
des steps Cucumber — jamais de nom/email qui ressemble à une vraie personne en dur
|
||||
dans un fixture. Les valeurs *littérales* des scénarios `.feature` eux-mêmes
|
||||
(ex. `alice@example.com`) restent volontairement statiques : c'est le point des
|
||||
scénarios Gherkin lisibles (exemples illustratifs conventionnels en BDD, pas des
|
||||
données réelles) — seules les données de remplissage hors du texte lisible du
|
||||
scénario sont générées.
|
||||
de test dans `test/*.test.ts` (Mocha) — jamais de nom/email qui ressemble à une
|
||||
vraie personne en dur dans un fixture.
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
// Explicit .cjs extension (not .js) so this loads as CommonJS regardless of
|
||||
// the "type": "module" in package.json — avoids the same ESM/CJS config
|
||||
// loader mismatch that broke apps/web/cypress.config.ts earlier.
|
||||
module.exports = {
|
||||
default: {
|
||||
paths: ["features/**/*.feature"],
|
||||
import: ["features/**/*.ts"],
|
||||
format: ["progress-bar"],
|
||||
formatOptions: { snippetInterface: "async-await" },
|
||||
},
|
||||
};
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
Feature: Account creation and login
|
||||
As a new user
|
||||
I want to create a profile and log in
|
||||
So that I can access my household's batch-cooking planning
|
||||
|
||||
Scenario: A visitor creates a new profile
|
||||
When I sign up with the following details:
|
||||
| firstName | Alice |
|
||||
| lastName | Martin |
|
||||
| email | alice@example.com |
|
||||
| password | correct-horse-battery-staple |
|
||||
Then the response status should be 201
|
||||
And I am authenticated as "alice@example.com"
|
||||
|
||||
Scenario: A visitor cannot sign up twice with the same email
|
||||
Given a profile already exists with email "alice@example.com"
|
||||
When I sign up with the following details:
|
||||
| firstName | Alice |
|
||||
| lastName | Martin |
|
||||
| email | alice@example.com |
|
||||
| password | correct-horse-battery-staple |
|
||||
Then the response status should be 409
|
||||
And the response error code should be "EMAIL_ALREADY_IN_USE"
|
||||
|
||||
Scenario: A registered user logs in with correct credentials
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
Then the response status should be 200
|
||||
And I am authenticated as "alice@example.com"
|
||||
|
||||
Scenario: A registered user cannot log in with the wrong password
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I log in with email "alice@example.com" and password "wrong-password"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "INVALID_CREDENTIALS"
|
||||
|
||||
Scenario: A signed-in user cannot delete their account with the wrong password
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I delete my account with password "wrong-password"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "INVALID_CREDENTIALS"
|
||||
And I am authenticated as "alice@example.com"
|
||||
|
||||
Scenario: A signed-in user deletes their account
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I delete my account with password "correct-horse-battery-staple"
|
||||
Then the response status should be 204
|
||||
And I am no longer authenticated
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
Feature: API health check
|
||||
As a monitoring service
|
||||
I want to query the API
|
||||
So that I can verify it is up and responding correctly
|
||||
|
||||
Scenario: The API is available
|
||||
When I send a GET request to "/health"
|
||||
Then the response status should be 200
|
||||
And the response body should be:
|
||||
"""
|
||||
{ "status": "ok" }
|
||||
"""
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
Feature: Household
|
||||
As a signed-in user
|
||||
I want to name my household, invite others to it, and manage its members
|
||||
So that my whole household can share the same planning
|
||||
|
||||
Scenario: A visitor without a session cannot read the household
|
||||
When I send a GET request to "/house/current"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user creates a household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I create a household named "Chez Alice"
|
||||
Then the response status should be 201
|
||||
And my household should be named "Chez Alice"
|
||||
|
||||
Scenario: A signed-in user renames their household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Foyer de test"
|
||||
When I rename my household to "Chez les Martin"
|
||||
Then the response status should be 200
|
||||
And my household should be named "Chez les Martin"
|
||||
|
||||
Scenario: A second user joins a household using its invite code
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
When the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
Then the second user's response status should be 200
|
||||
And the second user should be a member of my household
|
||||
|
||||
Scenario: A non-admin member cannot delete the household
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When the second user tries to delete the household
|
||||
Then the second user's response status should be 403
|
||||
And the second user's response error code should be "NOT_HOUSE_ADMIN"
|
||||
|
||||
Scenario: Adminship transfers to the remaining member when the admin leaves
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When I leave the household
|
||||
Then the response status should be 204
|
||||
And the second user should be the household's admin
|
||||
|
||||
Scenario: The admin removes a member
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I have a household named "Chez Alice"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user joins my household using its invite code
|
||||
When I remove the second user from my household
|
||||
Then the response status should be 200
|
||||
And the second user should have no household
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
Feature: Household weekly planning
|
||||
As a signed-in user
|
||||
I want to see my household's current planning
|
||||
So that I know what meals are planned this week
|
||||
|
||||
Scenario: A visitor without a session cannot view the planning
|
||||
When I request the current planning
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user with no planning yet sees an empty state
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I request the current planning
|
||||
Then the response status should be 200
|
||||
And the current planning response should be empty
|
||||
|
||||
Scenario: A signed-in user sees their household's current planning
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And my household has a planning covering today with recipe "Ratatouille" on "monday" for "dinner"
|
||||
When I request the current planning
|
||||
Then the response status should be 200
|
||||
And the current planning response should include recipe "Ratatouille" on "monday" for "dinner"
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Feature: User preferences (theme)
|
||||
As a signed-in user
|
||||
I want to choose a light, dark, or system theme
|
||||
So that the app matches how I like to read it
|
||||
|
||||
Scenario: A visitor without a session cannot read preferences
|
||||
When I send a GET request to "/preferences"
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user with no preferences yet defaults to SYSTEM
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I request my preferences
|
||||
Then the response status should be 200
|
||||
And my theme preference should be "SYSTEM"
|
||||
|
||||
Scenario: A signed-in user sets their theme preference
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I set my theme preference to "DARK"
|
||||
Then the response status should be 200
|
||||
And my theme preference should be "DARK"
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
Feature: Profile regime and allergens
|
||||
As a signed-in user
|
||||
I want to set my dietary regime and allergens/intolerances
|
||||
So that the household's meal planning can account for them later
|
||||
|
||||
Scenario: A visitor without a session cannot set a regime
|
||||
When I send a PATCH request to "/profile/diet" with body:
|
||||
"""
|
||||
{ "dietId": 1 }
|
||||
"""
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user sets their regime to a valid, seeded diet
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I set my regime to "Végétarien"
|
||||
Then the response status should be 200
|
||||
And my profile's regime should be "Végétarien"
|
||||
|
||||
Scenario: A signed-in user selects allergens, then replaces the selection
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I set my allergens to "Arachides, Gluten"
|
||||
Then the response status should be 200
|
||||
And my selected allergens should be "Arachides, Gluten"
|
||||
When I set my allergens to "Lait"
|
||||
Then my selected allergens should be "Lait"
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
Feature: Recipe catalog
|
||||
As a signed-in user
|
||||
I want to browse, create and manage recipes
|
||||
So that the household can plan meals from a shared catalog
|
||||
|
||||
Scenario: A visitor without a session cannot browse the catalog
|
||||
When I request the recipe catalog
|
||||
Then the response status should be 401
|
||||
And the response error code should be "NOT_AUTHENTICATED"
|
||||
|
||||
Scenario: A signed-in user creates a recipe with an ingredient and a step
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I create a recipe named "Ratatouille" with ingredient "Tomate" and step "Couper les légumes"
|
||||
Then the response status should be 201
|
||||
And the created recipe should have ingredient "Tomate" and step "Couper les légumes"
|
||||
|
||||
Scenario: Creating a recipe with an unknown ingredient is rejected
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
When I create a recipe named "Ratatouille" with unknown ingredient id 999999 and step "Couper les légumes"
|
||||
Then the response status should be 404
|
||||
And the response error code should be "INGREDIENT_NOT_FOUND"
|
||||
|
||||
Scenario: A signed-in user sees their own recipe in the "perso" tab
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And a recipe named "Ratatouille" already exists with ingredient "Tomate" and step "Couper les légumes"
|
||||
When I request the recipe catalog tab "perso"
|
||||
Then the response status should be 200
|
||||
And the recipe catalog response should include "Ratatouille"
|
||||
|
||||
Scenario: A signed-in user favorites a recipe and finds it in the "favoris" tab
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And a recipe named "Ratatouille" already exists with ingredient "Tomate" and step "Couper les légumes"
|
||||
When I favorite the recipe named "Ratatouille"
|
||||
And I request the recipe catalog tab "favoris"
|
||||
Then the response status should be 200
|
||||
And the recipe catalog response should include "Ratatouille"
|
||||
|
||||
Scenario: Only a recipe's author can edit it
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And a public recipe named "Ratatouille" already exists with ingredient "Tomate" and step "Couper les légumes"
|
||||
And a profile already exists with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
And the second user logs in with email "bob@example.com" and password "correct-horse-battery-staple"
|
||||
When the second user tries to modify the recipe named "Ratatouille"
|
||||
Then the second user's response status should be 403
|
||||
And the second user's response error code should be "NOT_RECIPE_AUTHOR"
|
||||
|
||||
Scenario: Deleting a recipe still used by a planning item is rejected
|
||||
Given a profile already exists with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And I log in with email "alice@example.com" and password "correct-horse-battery-staple"
|
||||
And a recipe named "Ratatouille" already exists with ingredient "Tomate" and step "Couper les légumes"
|
||||
And my household has a planning that uses the recipe named "Ratatouille"
|
||||
When I delete the recipe named "Ratatouille"
|
||||
Then the response status should be 409
|
||||
And the response error code should be "RECIPE_IN_USE"
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Feature: Reference data (diets, allergens)
|
||||
As a visitor filling in the signup wizard, or a signed-in user editing their profile
|
||||
I want to read the list of dietary regimes and allergens
|
||||
So that I can pick from them — before an account necessarily exists
|
||||
|
||||
Scenario: A visitor without a session can read the list of dietary regimes
|
||||
When I send a GET request to "/reference/diets"
|
||||
Then the response status should be 200
|
||||
And the reference list response should include "Végétarien"
|
||||
|
||||
Scenario: A visitor without a session can read the list of allergens
|
||||
When I send a GET request to "/reference/allergies"
|
||||
Then the response status should be 200
|
||||
And the reference list response should include "Arachides"
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import type { DataTable } from "@cucumber/cucumber";
|
||||
import { Given, Then, When } from "@cucumber/cucumber";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { signup } from "../../src/modules/auth/auth.service.js";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
// firstName/lastName/password below are filler for background state the
|
||||
// scenario doesn't actually read (only the emails in the .feature file are
|
||||
// part of what's being tested) — faker-generated rather than hardcoded so
|
||||
// no test fixture ever looks like a real person's data.
|
||||
|
||||
Given("a profile already exists with email {string}", async (email: string) => {
|
||||
await signup({
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
email,
|
||||
password: faker.internet.password({ length: 16 }),
|
||||
});
|
||||
});
|
||||
|
||||
Given(
|
||||
"a profile already exists with email {string} and password {string}",
|
||||
async (email: string, password: string) => {
|
||||
await signup({
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
email,
|
||||
password,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
When("I sign up with the following details:", async function (this: CustomWorld, table: DataTable) {
|
||||
const details = table.rowsHash();
|
||||
this.response = await this.agent.post("/auth/signup").send({
|
||||
firstName: details.firstName,
|
||||
lastName: details.lastName,
|
||||
email: details.email,
|
||||
password: details.password,
|
||||
});
|
||||
});
|
||||
|
||||
When(
|
||||
"I log in with email {string} and password {string}",
|
||||
async function (this: CustomWorld, email: string, password: string) {
|
||||
this.response = await this.agent.post("/auth/login").send({ email, password });
|
||||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"I delete my account with password {string}",
|
||||
async function (this: CustomWorld, password: string) {
|
||||
this.response = await this.agent.delete("/auth/me").send({ password });
|
||||
},
|
||||
);
|
||||
|
||||
Then("I am authenticated as {string}", async function (this: CustomWorld, email: string) {
|
||||
const res = await this.agent.get("/auth/me");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.body.email, email);
|
||||
});
|
||||
|
||||
Then("I am no longer authenticated", async function (this: CustomWorld) {
|
||||
const res = await this.agent.get("/auth/me");
|
||||
assert.equal(res.status, 401);
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { ErrorCode } from "@batch-cooking/shared";
|
||||
import { Then, When } from "@cucumber/cucumber";
|
||||
import request from "supertest";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
When("I send a GET request to {string}", async function (this: CustomWorld, path: string) {
|
||||
this.response = await request(this.app).get(path);
|
||||
});
|
||||
|
||||
When(
|
||||
"I send a PATCH request to {string} with body:",
|
||||
async function (this: CustomWorld, path: string, body: string) {
|
||||
this.response = await request(this.app).patch(path).send(JSON.parse(body));
|
||||
},
|
||||
);
|
||||
|
||||
Then("the response status should be {int}", function (this: CustomWorld, status: number) {
|
||||
assert.equal(this.response.status, status);
|
||||
});
|
||||
|
||||
Then("the response body should be:", function (this: CustomWorld, expectedJson: string) {
|
||||
assert.deepEqual(this.response.body, JSON.parse(expectedJson));
|
||||
});
|
||||
|
||||
// Generic enough to be reused by any feature asserting on the shared
|
||||
// ApiErrorResponse contract's `code` field — not health-specific, but this
|
||||
// file is where the other generic response-assertion steps already live.
|
||||
//
|
||||
// `code` here is the enum *member name* (readable in the .feature file,
|
||||
// e.g. "EMAIL_ALREADY_IN_USE") — ErrorCode[name] resolves it to the real
|
||||
// numeric value via TypeScript's reverse enum lookup, so this never
|
||||
// compares against a hardcoded number.
|
||||
Then("the response error code should be {string}", function (this: CustomWorld, code: string) {
|
||||
const expected = ErrorCode[code as keyof typeof ErrorCode];
|
||||
assert.notEqual(expected, undefined, `Unknown ErrorCode member: "${code}"`);
|
||||
assert.equal(this.response.body.code, expected);
|
||||
});
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { ErrorCode } from "@batch-cooking/shared";
|
||||
import { Given, Then, When } from "@cucumber/cucumber";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
Given("I have a household named {string}", async function (this: CustomWorld, name: string) {
|
||||
const res = await this.agent.post("/house").send({ name });
|
||||
assert.equal(res.status, 201, JSON.stringify(res.body));
|
||||
});
|
||||
|
||||
When("I create a household named {string}", async function (this: CustomWorld, name: string) {
|
||||
this.response = await this.agent.post("/house").send({ name });
|
||||
});
|
||||
|
||||
When("I rename my household to {string}", async function (this: CustomWorld, name: string) {
|
||||
this.response = await this.agent.patch("/house/current").send({ name });
|
||||
});
|
||||
|
||||
When("I leave the household", async function (this: CustomWorld) {
|
||||
this.response = await this.agent.post("/house/leave");
|
||||
});
|
||||
|
||||
When("I remove the second user from my household", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
this.response = await this.agent.delete(`/house/members/${secondMe.body.id}`);
|
||||
});
|
||||
|
||||
Then("my household should be named {string}", async function (this: CustomWorld, name: string) {
|
||||
const res = await this.agent.get("/house/current");
|
||||
assert.equal(res.body.name, name);
|
||||
});
|
||||
|
||||
// --- Steps involving a second, independently signed-in user ---------------
|
||||
// The first ("a profile already exists with email ...") step is reused
|
||||
// as-is for the second user too — it just inserts a row, agent-agnostic.
|
||||
|
||||
When(
|
||||
"the second user logs in with email {string} and password {string}",
|
||||
async function (this: CustomWorld, email: string, password: string) {
|
||||
this.secondResponse = await this.secondAgent.post("/auth/login").send({ email, password });
|
||||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"the second user joins my household using its invite code",
|
||||
async function (this: CustomWorld) {
|
||||
const house = await this.agent.get("/house/current");
|
||||
this.secondResponse = await this.secondAgent
|
||||
.post("/house/join")
|
||||
.send({ inviteCode: house.body.inviteCode });
|
||||
},
|
||||
);
|
||||
|
||||
When("the second user tries to delete the household", async function (this: CustomWorld) {
|
||||
this.secondResponse = await this.secondAgent.delete("/house/current");
|
||||
});
|
||||
|
||||
Then(
|
||||
"the second user's response status should be {int}",
|
||||
function (this: CustomWorld, status: number) {
|
||||
assert.equal(this.secondResponse.status, status);
|
||||
},
|
||||
);
|
||||
|
||||
Then(
|
||||
"the second user's response error code should be {string}",
|
||||
function (this: CustomWorld, code: string) {
|
||||
const expected = ErrorCode[code as keyof typeof ErrorCode];
|
||||
assert.notEqual(expected, undefined, `Unknown ErrorCode member: "${code}"`);
|
||||
assert.equal(this.secondResponse.body.code, expected);
|
||||
},
|
||||
);
|
||||
|
||||
Then("the second user should be a member of my household", async function (this: CustomWorld) {
|
||||
const house = await this.agent.get("/house/current");
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
const memberIds = (house.body.members as Array<{ id: number }>).map((member) => member.id);
|
||||
assert.ok(memberIds.includes(secondMe.body.id));
|
||||
});
|
||||
|
||||
Then("the second user should be the household's admin", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
const house = await this.secondAgent.get("/house/current");
|
||||
assert.equal(house.body.adminId, secondMe.body.id);
|
||||
});
|
||||
|
||||
Then("the second user should have no household", async function (this: CustomWorld) {
|
||||
const secondMe = await this.secondAgent.get("/auth/me");
|
||||
assert.equal(secondMe.body.houseId, null);
|
||||
});
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Given, Then, When } from "@cucumber/cucumber";
|
||||
import { prisma } from "../../src/db/prisma.js";
|
||||
import { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
/** `GET /planning` takes `?date=` explicitly — this scenario wording ("the current planning") maps to the fixed test "today" (see `TEST_REFERENCE_DATE`). */
|
||||
When("I request the current planning", async function (this: CustomWorld) {
|
||||
this.response = await this.agent
|
||||
.get("/planning")
|
||||
.query({ date: TEST_REFERENCE_DATE.toISODate() });
|
||||
});
|
||||
|
||||
Then("the current planning response should be empty", function (this: CustomWorld) {
|
||||
assert.equal(this.response.body, null);
|
||||
});
|
||||
|
||||
// Creates the planning/recipe rows directly via Prisma rather than through
|
||||
// the API — there's no "create a planning" endpoint yet (see
|
||||
// specs/batch-cooking-architecture.md, "Calcul batch-cooking" is still
|
||||
// TODO), so this is the only way to get a household into a state where it
|
||||
// has one. A household is no longer created implicitly at signup, so this
|
||||
// step creates one via `POST /house` first — the scenario never names it
|
||||
// explicitly, its name doesn't matter here.
|
||||
Given(
|
||||
"my household has a planning covering today with recipe {string} on {string} for {string}",
|
||||
async function (this: CustomWorld, recipeName: string, weekDay: string, meal: string) {
|
||||
const houseRes = await this.agent.post("/house").send({ name: "Foyer de test" });
|
||||
const houseId: number = houseRes.body.id;
|
||||
|
||||
const recipe = await prisma.recipe.create({
|
||||
data: { name: recipeName, authorId: houseRes.body.adminId },
|
||||
});
|
||||
const planning = await prisma.planning.create({
|
||||
data: {
|
||||
houseId,
|
||||
startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(),
|
||||
finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(),
|
||||
},
|
||||
});
|
||||
await prisma.planningItem.create({
|
||||
data: { planningId: planning.id, weekDay, meal, recipeId: recipe.id },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Then(
|
||||
"the current planning response should include recipe {string} on {string} for {string}",
|
||||
function (this: CustomWorld, recipeName: string, weekDay: string, meal: string) {
|
||||
const items = this.response.body.items as Array<{
|
||||
weekDay: string;
|
||||
meal: string;
|
||||
recipe: { name: string };
|
||||
}>;
|
||||
const item = items.find((i) => i.recipe.name === recipeName);
|
||||
assert.ok(item, `expected an item with recipe "${recipeName}", got ${JSON.stringify(items)}`);
|
||||
assert.equal(item.weekDay, weekDay);
|
||||
assert.equal(item.meal, meal);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Then, When } from "@cucumber/cucumber";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
When("I request my preferences", async function (this: CustomWorld) {
|
||||
this.response = await this.agent.get("/preferences");
|
||||
});
|
||||
|
||||
When("I set my theme preference to {string}", async function (this: CustomWorld, theme: string) {
|
||||
this.response = await this.agent.patch("/preferences").send({ theme });
|
||||
});
|
||||
|
||||
Then("my theme preference should be {string}", function (this: CustomWorld, theme: string) {
|
||||
assert.equal(this.response.body.theme, theme);
|
||||
});
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Then, When } from "@cucumber/cucumber";
|
||||
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
|
||||
import { prisma } from "../../src/db/prisma.js";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
/** Splits a comma-separated list of names from a `.feature` string into trimmed, non-empty parts. */
|
||||
function splitNames(names: string): string[] {
|
||||
return names
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves allergen names (as written in a `.feature` file, e.g.
|
||||
* "Arachides") to their Allergy id — scenarios still name allergens by their
|
||||
* French label for readability, so this slugifies before matching against
|
||||
* `Category.key` (see reference.service.ts for why the key lives on
|
||||
* Category, not Allergy).
|
||||
*/
|
||||
async function allergyIdsFor(names: string[]): Promise<number[]> {
|
||||
const allergies = await prisma.allergy.findMany({ include: { category: true } });
|
||||
return names.map((name) => {
|
||||
const key = getEnglishKey(name);
|
||||
const match = allergies.find((allergy) => allergy.category.key === key);
|
||||
if (!match) throw new Error(`No seeded allergen named "${name}"`);
|
||||
return match.id;
|
||||
});
|
||||
}
|
||||
|
||||
When("I set my regime to {string}", async function (this: CustomWorld, dietName: string) {
|
||||
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey(dietName) } });
|
||||
this.response = await this.agent.patch("/profile/diet").send({ dietId: diet.id });
|
||||
});
|
||||
|
||||
Then(
|
||||
"my profile's regime should be {string}",
|
||||
async function (this: CustomWorld, dietName: string) {
|
||||
const diet = await prisma.diet.findFirstOrThrow({ where: { key: getEnglishKey(dietName) } });
|
||||
assert.equal(this.response.body.dietId, diet.id);
|
||||
},
|
||||
);
|
||||
|
||||
When("I set my allergens to {string}", async function (this: CustomWorld, names: string) {
|
||||
const allergyIds = await allergyIdsFor(splitNames(names));
|
||||
this.response = await this.agent.patch("/profile/allergies").send({ allergyIds });
|
||||
});
|
||||
|
||||
Then("my selected allergens should be {string}", async function (this: CustomWorld, names: string) {
|
||||
const expected = (await allergyIdsFor(splitNames(names))).sort();
|
||||
const actual = [...this.response.body].sort();
|
||||
assert.deepEqual(actual, expected);
|
||||
});
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Given, Then, When } from "@cucumber/cucumber";
|
||||
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
|
||||
import { prisma } from "../../src/db/prisma.js";
|
||||
import { TEST_REFERENCE_DATE } from "../../test-support/reference-date.js";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
/**
|
||||
* Resolves a reference ingredient by its seeded French name — every
|
||||
* scenario below names an ingredient by its `reference-seed-data.ts` name,
|
||||
* never a raw id or its slug `key` directly, so this slugifies before
|
||||
* matching.
|
||||
*/
|
||||
async function findIngredientId(name: string): Promise<number> {
|
||||
const ingredient = await prisma.ingredient.findFirstOrThrow({
|
||||
where: { key: getEnglishKey(name) },
|
||||
});
|
||||
return ingredient.id;
|
||||
}
|
||||
|
||||
When("I request the recipe catalog", async function (this: CustomWorld) {
|
||||
this.response = await this.agent.get("/recipes");
|
||||
});
|
||||
|
||||
When("I request the recipe catalog tab {string}", async function (this: CustomWorld, tab: string) {
|
||||
this.response = await this.agent.get("/recipes").query({ tab });
|
||||
});
|
||||
|
||||
Then(
|
||||
"the recipe catalog response should include {string}",
|
||||
function (this: CustomWorld, name: string) {
|
||||
const names = (this.response.body as Array<{ name: string }>).map((recipe) => recipe.name);
|
||||
assert.ok(names.includes(name), `expected ${JSON.stringify(names)} to include "${name}"`);
|
||||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"I create a recipe named {string} with ingredient {string} and step {string}",
|
||||
async function (this: CustomWorld, name: string, ingredientName: string, step: string) {
|
||||
const ingredientId = await findIngredientId(ingredientName);
|
||||
this.response = await this.agent.post("/recipes").send({
|
||||
name,
|
||||
dietIds: [],
|
||||
ingredients: [{ ingredientId, quantity: 1, unit: "unité" }],
|
||||
steps: [{ description: step }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
When(
|
||||
"I create a recipe named {string} with unknown ingredient id {int} and step {string}",
|
||||
async function (this: CustomWorld, name: string, unknownIngredientId: number, step: string) {
|
||||
this.response = await this.agent.post("/recipes").send({
|
||||
name,
|
||||
dietIds: [],
|
||||
ingredients: [{ ingredientId: unknownIngredientId, quantity: 1, unit: "unité" }],
|
||||
steps: [{ description: step }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Then(
|
||||
"the created recipe should have ingredient {string} and step {string}",
|
||||
function (this: CustomWorld, ingredientName: string, step: string) {
|
||||
const body = this.response.body as {
|
||||
ingredients: Array<{ ingredient: { key: string } }>;
|
||||
steps: Array<{ description: string }>;
|
||||
};
|
||||
const expectedKey = getEnglishKey(ingredientName);
|
||||
assert.ok(body.ingredients.some((line) => line.ingredient.key === expectedKey));
|
||||
assert.ok(body.steps.some((s) => s.description === step));
|
||||
},
|
||||
);
|
||||
|
||||
// Created directly via Prisma (with a nested ingredient + step), not through
|
||||
// the API — same rationale as `planning.steps.ts`'s equivalent "already
|
||||
// exists" step: this is background state the scenario needs in place before
|
||||
// its actual `When`, not the behavior under test. `authorId` is the
|
||||
// currently-logged-in agent's own profile — `visibility` defaults to
|
||||
// `PERSONAL` (schema.prisma), matching a recipe this agent just created for
|
||||
// themselves.
|
||||
Given(
|
||||
"a recipe named {string} already exists with ingredient {string} and step {string}",
|
||||
async function (this: CustomWorld, name: string, ingredientName: string, step: string) {
|
||||
const ingredientId = await findIngredientId(ingredientName);
|
||||
const me = await this.agent.get("/auth/me");
|
||||
await prisma.recipe.create({
|
||||
data: {
|
||||
name,
|
||||
authorId: me.body.id,
|
||||
ingredients: { create: [{ ingredientId, quantity: 1, unit: "unité" }] },
|
||||
steps: { create: [{ description: step, order: 0 }] },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Same as above but `visibility: PUBLIC` — needed for scenarios where a
|
||||
// *second* user must be able to see (though not necessarily edit) the
|
||||
// recipe, e.g. the "only the author can edit" scenario: a `PERSONAL`
|
||||
// recipe would 404 for anyone else before the authorship check even runs
|
||||
// (see `recipe.service.ts`'s `canView`).
|
||||
Given(
|
||||
"a public recipe named {string} already exists with ingredient {string} and step {string}",
|
||||
async function (this: CustomWorld, name: string, ingredientName: string, step: string) {
|
||||
const ingredientId = await findIngredientId(ingredientName);
|
||||
const me = await this.agent.get("/auth/me");
|
||||
await prisma.recipe.create({
|
||||
data: {
|
||||
name,
|
||||
authorId: me.body.id,
|
||||
visibility: "PUBLIC",
|
||||
ingredients: { create: [{ ingredientId, quantity: 1, unit: "unité" }] },
|
||||
steps: { create: [{ description: step, order: 0 }] },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Distinct from `planning.steps.ts`'s "my household has a planning covering
|
||||
// today with recipe {string}..." — that step always creates a *new* recipe
|
||||
// row with the given name, which wouldn't exercise the actual `RECIPE_IN_USE`
|
||||
// check against a recipe this feature already created. This step instead
|
||||
// looks up the already-existing recipe by name and points the planning item
|
||||
// at its real id.
|
||||
Given(
|
||||
"my household has a planning that uses the recipe named {string}",
|
||||
async function (this: CustomWorld, recipeName: string) {
|
||||
const houseRes = await this.agent.post("/house").send({ name: "Foyer de test" });
|
||||
const houseId: number = houseRes.body.id;
|
||||
const recipe = await prisma.recipe.findFirstOrThrow({ where: { name: recipeName } });
|
||||
|
||||
const planning = await prisma.planning.create({
|
||||
data: {
|
||||
houseId,
|
||||
startDate: TEST_REFERENCE_DATE.minus({ days: 2 }).toJSDate(),
|
||||
finishDate: TEST_REFERENCE_DATE.plus({ days: 2 }).toJSDate(),
|
||||
},
|
||||
});
|
||||
await prisma.planningItem.create({
|
||||
data: { planningId: planning.id, weekDay: "lundi", meal: "diner", recipeId: recipe.id },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
When("I delete the recipe named {string}", async function (this: CustomWorld, name: string) {
|
||||
const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } });
|
||||
this.response = await this.agent.delete(`/recipes/${recipe.id}`);
|
||||
});
|
||||
|
||||
When("I favorite the recipe named {string}", async function (this: CustomWorld, name: string) {
|
||||
const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } });
|
||||
this.response = await this.agent.post(`/recipes/${recipe.id}/favorite`);
|
||||
});
|
||||
|
||||
When(
|
||||
"the second user tries to modify the recipe named {string}",
|
||||
async function (this: CustomWorld, name: string) {
|
||||
const ingredientId = await findIngredientId("Tomate");
|
||||
const recipe = await prisma.recipe.findFirstOrThrow({ where: { name } });
|
||||
this.secondResponse = await this.secondAgent.patch(`/recipes/${recipe.id}`).send({
|
||||
name,
|
||||
dietIds: [],
|
||||
ingredients: [{ ingredientId, quantity: 1, unit: "unité" }],
|
||||
steps: [{ description: "Hack" }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { Then } from "@cucumber/cucumber";
|
||||
import { getEnglishKey } from "../../src/db/catalog-en-keys.js";
|
||||
import type { CustomWorld } from "../support/world.js";
|
||||
|
||||
// The feature file still names an item by its French label, for
|
||||
// readability — the response itself carries only the slug `key` (see
|
||||
// reference.service.ts), so this slugifies the expected label before
|
||||
// comparing.
|
||||
Then(
|
||||
"the reference list response should include {string}",
|
||||
function (this: CustomWorld, name: string) {
|
||||
const keys = (this.response.body as Array<{ key: string }>).map((item) => item.key);
|
||||
const expectedKey = getEnglishKey(name);
|
||||
assert.ok(
|
||||
keys.includes(expectedKey),
|
||||
`expected ${JSON.stringify(keys)} to include "${expectedKey}"`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Before, setDefaultTimeout } from "@cucumber/cucumber";
|
||||
import { resetDatabase } from "../../test-support/reset-db.js";
|
||||
|
||||
// Integration tests hitting a real Postgres + argon2 need more than
|
||||
// Cucumber's 5s default, especially on a cold Prisma connection.
|
||||
setDefaultTimeout(10_000);
|
||||
|
||||
Before(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { type IWorldOptions, World, setWorldConstructor } from "@cucumber/cucumber";
|
||||
import type { Express } from "express";
|
||||
import request from "supertest";
|
||||
import { createApp } from "../../src/app.js";
|
||||
|
||||
// Fresh Express app per scenario (in-process, via supertest — no server to
|
||||
// spin up/tear down) plus the last HTTP response, available to every step.
|
||||
// `agent` persists cookies across requests within a scenario (needed for
|
||||
// "sign up then check I'm authenticated" style flows).
|
||||
export class CustomWorld extends World {
|
||||
app: Express;
|
||||
agent: ReturnType<typeof request.agent>;
|
||||
response!: request.Response;
|
||||
/** A second, independent session (own cookie jar) — only used by scenarios needing two distinct signed-in users, e.g. household invites/admin transfer/member removal. */
|
||||
secondAgent: ReturnType<typeof request.agent>;
|
||||
secondResponse!: request.Response;
|
||||
|
||||
constructor(options: IWorldOptions) {
|
||||
super(options);
|
||||
this.app = createApp();
|
||||
this.agent = request.agent(this.app);
|
||||
this.secondAgent = request.agent(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
setWorldConstructor(CustomWorld);
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/server.js",
|
||||
"test": "cross-env NODE_ENV=test mocha",
|
||||
"test:bdd": "cross-env NODE_ENV=test NODE_OPTIONS=--import=tsx cucumber-js",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:seed": "prisma db seed",
|
||||
|
|
@ -31,7 +30,6 @@
|
|||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cucumber/cucumber": "^13.2.1",
|
||||
"@faker-js/faker": "^10.6.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
|
|
|
|||
577
pnpm-lock.yaml
577
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue