import { test } from '@japa/runner' import testUtils from '@adonisjs/core/services/test_utils' import User from '#models/user' import Plan from '#models/plan' import { createTestUser } from '../helpers/auth.js' /** * Tests d'auth : signup, login, refresh, logout, perms. * * `withGlobalTransaction` wrap chaque test dans une tx PG qui est * rollback à la fin → pas besoin de truncate, isolation parfaite. */ test.group('Auth — POST /auth/signup', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('crée user + org + 4 plans pré-fournis dans une tx', async ({ client, assert }) => { const response = await client.post('/api/v1/auth/signup').json({ email: 'alice@spec.test', password: 'password123', fullName: 'Alice Spec', }) response.assertStatus(201) const data = response.body().data assert.properties(data, ['accessToken', 'expiresAt', 'user']) assert.properties(data.user, ['id', 'email', 'organizationId', 'fullName']) assert.equal(data.user.email, 'alice@spec.test') // L'org est créée mais le nom reste vide (rempli en onboarding). const user = await User.findByOrFail('email', 'alice@spec.test') assert.isNotNull(user.organizationId) // Les 4 plans pré-fournis sont en place pour cette org. const plans = await Plan.query().where('organization_id', user.organizationId!) assert.lengthOf(plans, 4) const slugs = plans.map((p) => p.slug).sort() assert.deepEqual(slugs, ['ferme-7j', 'patient-60j', 'rapide-15j', 'standard-30j']) }) test('refuse un email mal formé (422)', async ({ client }) => { const response = await client.post('/api/v1/auth/signup').json({ email: 'pas-un-email', password: 'password123', fullName: 'Test', }) response.assertStatus(422) }) test('refuse un password < 8 chars (422)', async ({ client }) => { const response = await client.post('/api/v1/auth/signup').json({ email: 'short@spec.test', password: 'abc', fullName: 'Test', }) response.assertStatus(422) }) test('refuse un email déjà pris (422 + erreur typée sur le champ email)', async ({ client, assert, }) => { await client.post('/api/v1/auth/signup').json({ email: 'twice@spec.test', password: 'password123', fullName: 'First', }) const response = await client.post('/api/v1/auth/signup').json({ email: 'twice@spec.test', password: 'password123', fullName: 'Second', }) response.assertStatus(422) // Vérifie le format précis de la réponse — pas juste un 422 vague. // Permet de détecter une régression où : // 1. La validation unique disparait (validator Vine ou contrainte DB) // 2. Le handler d'exception change la shape (`{ errors: [{ ... }] }`) // 3. Le `field` n'est plus posé sur l'erreur (le SPA s'en sert pour // mettre en rouge le bon input) // // Ce test a été enrichi suite à un mutation test du 2026-05-18 qui a // révélé qu'un seul `assertStatus(422)` laissait passer le retrait // du `.unique()` côté validator (la contrainte DB prenait le relais // silencieusement, sans test). const body = response.body() as { errors?: Array<{ code?: string; field?: string; message?: string }> } assert.isArray(body.errors) assert.isAtLeast(body.errors!.length, 1) const emailError = body.errors!.find((e) => e.field === 'email') assert.exists( emailError, "le payload d'erreur doit avoir au moins une entrée avec field='email'" ) assert.isString(emailError!.code) assert.isString(emailError!.message) }) }) test.group('Auth — POST /auth/login', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('émet une AuthSession avec credentials valides', async ({ client, assert }) => { const { user, plainPassword } = await createTestUser() const response = await client.post('/api/v1/auth/login').json({ email: user.email, password: plainPassword, }) response.assertStatus(200) const data = response.body().data assert.equal(data.user.email, user.email) assert.isString(data.accessToken) }) test('rejette un mauvais password (401)', async ({ client }) => { const { user } = await createTestUser() const response = await client.post('/api/v1/auth/login').json({ email: user.email, password: 'wrong-password', }) response.assertStatus(401) }) test('rejette un email inconnu (401)', async ({ client }) => { const response = await client.post('/api/v1/auth/login').json({ email: 'ghost@spec.test', password: 'password123', }) response.assertStatus(401) }) }) test.group('Auth — bearer requis', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('GET /account/profile → 401 sans token', async ({ client }) => { const response = await client.get('/api/v1/account/profile') response.assertStatus(401) }) test('GET /account/profile → 401 avec un token bidon', async ({ client }) => { const response = await client .get('/api/v1/account/profile') .header('Authorization', 'Bearer invalid-token') response.assertStatus(401) }) test('GET /account/profile → 200 avec token valide', async ({ client, assert }) => { const { user, accessToken } = await createTestUser() const response = await client .get('/api/v1/account/profile') .header('Authorization', `Bearer ${accessToken}`) response.assertStatus(200) assert.equal(response.body().data.email, user.email) }) }) test.group('Auth — POST /account/logout', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('révoque le token courant (204)', async ({ client }) => { const { accessToken } = await createTestUser() const logout = await client .post('/api/v1/account/logout') .header('Authorization', `Bearer ${accessToken}`) logout.assertStatus(204) // Le token doit être révoqué : la requête suivante revient 401. const after = await client .get('/api/v1/account/profile') .header('Authorization', `Bearer ${accessToken}`) after.assertStatus(401) }) }) test.group('Auth — onboarding state', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('PATCH /organizations/me pose onboardingCompletedAt à la 1re mise du nom', async ({ client, assert, }) => { const { accessToken } = await createTestUser() // 1er PATCH : pose onboardingCompletedAt const r1 = await client .patch('/api/v1/organizations/me') .header('Authorization', `Bearer ${accessToken}`) .json({ name: 'Boulangerie Spec' }) r1.assertStatus(200) assert.isNotNull(r1.body().data.onboardingCompletedAt) // 2e PATCH : ne touche pas onboardingCompletedAt (déjà set) const onboardedAt = r1.body().data.onboardingCompletedAt const r2 = await client .patch('/api/v1/organizations/me') .header('Authorization', `Bearer ${accessToken}`) .json({ name: 'Boulangerie Spec 2' }) r2.assertStatus(200) assert.equal(r2.body().data.onboardingCompletedAt, onboardedAt) }) })