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)
143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
import { test } from '@japa/runner'
|
|
import testUtils from '@adonisjs/core/services/test_utils'
|
|
import PlanStep from '#models/plan_step'
|
|
import { createTestUser, createTwoOrgs } from '../helpers/auth.js'
|
|
import { body, type ApiOk } from '../helpers/response.js'
|
|
|
|
type PlanShape = {
|
|
id: string
|
|
slug: string | null
|
|
name: string
|
|
description: string
|
|
isDefault: boolean
|
|
steps: Array<{ id: string; order: number; tone: string }>
|
|
usageCount?: number
|
|
}
|
|
|
|
test.group('Plans — GET /plans', (group) => {
|
|
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
|
|
|
test('retourne les 4 plans pré-fournis avec steps préchargés', async ({
|
|
client,
|
|
assert,
|
|
}) => {
|
|
const { bearer } = await createTestUser()
|
|
const r = await client.get('/api/v1/plans').headers(bearer)
|
|
r.assertStatus(200)
|
|
|
|
const plans = body<ApiOk<PlanShape[]>>(r).data
|
|
assert.lengthOf(plans, 4)
|
|
for (const p of plans) {
|
|
assert.isTrue(p.isDefault)
|
|
assert.isAbove(p.steps.length, 0)
|
|
assert.equal(p.usageCount, 0)
|
|
}
|
|
})
|
|
|
|
test('isolation cross-org : chaque org voit ses propres copies', async ({
|
|
client,
|
|
assert,
|
|
}) => {
|
|
const { a, b } = await createTwoOrgs()
|
|
const fromA = await client.get('/api/v1/plans').headers(a.bearer)
|
|
const fromB = await client.get('/api/v1/plans').headers(b.bearer)
|
|
|
|
const idsA = body<ApiOk<PlanShape[]>>(fromA).data.map((p) => p.id).sort()
|
|
const idsB = body<ApiOk<PlanShape[]>>(fromB).data.map((p) => p.id).sort()
|
|
for (const id of idsA) assert.notInclude(idsB, id)
|
|
})
|
|
})
|
|
|
|
test.group('Plans — GET /plans/:slug', (group) => {
|
|
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
|
|
|
test('lookup par slug avec steps ordonnés', async ({ client, assert }) => {
|
|
const { bearer } = await createTestUser()
|
|
const r = await client.get('/api/v1/plans/standard-30j').headers(bearer)
|
|
r.assertStatus(200)
|
|
|
|
const plan = body<ApiOk<PlanShape>>(r).data
|
|
assert.equal(plan.slug, 'standard-30j')
|
|
const orders = plan.steps.map((s) => s.order)
|
|
assert.deepEqual(orders, [...orders].sort((a, b) => a - b))
|
|
})
|
|
|
|
test('404 si slug inconnu', async ({ client }) => {
|
|
const { bearer } = await createTestUser()
|
|
const r = await client.get('/api/v1/plans/inexistant').headers(bearer)
|
|
r.assertStatus(404)
|
|
})
|
|
})
|
|
|
|
test.group('Plans — PATCH /plans/:slug', (group) => {
|
|
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
|
|
|
test('édite name + remplace les steps en bloc', async ({ client, assert }) => {
|
|
const { bearer } = await createTestUser()
|
|
|
|
const before = await client.get('/api/v1/plans/standard-30j').headers(bearer)
|
|
const planId = body<ApiOk<PlanShape>>(before).data.id
|
|
|
|
const r = await client
|
|
.patch('/api/v1/plans/standard-30j')
|
|
.headers(bearer)
|
|
.json({
|
|
name: 'Standard édité',
|
|
steps: [
|
|
{
|
|
order: 0,
|
|
offsetDays: 5,
|
|
tone: 'amical',
|
|
subject: 'Hop',
|
|
body: 'Hop hop',
|
|
requiresManualValidation: false,
|
|
},
|
|
],
|
|
})
|
|
|
|
r.assertStatus(200)
|
|
const data = body<ApiOk<PlanShape>>(r).data
|
|
assert.equal(data.name, 'Standard édité')
|
|
assert.lengthOf(data.steps, 1)
|
|
|
|
const steps = await PlanStep.query().where('plan_id', planId)
|
|
assert.lengthOf(steps, 1)
|
|
})
|
|
|
|
test('rejette tone invalide (422)', async ({ client }) => {
|
|
const { bearer } = await createTestUser()
|
|
const r = await client
|
|
.patch('/api/v1/plans/standard-30j')
|
|
.headers(bearer)
|
|
.json({
|
|
steps: [
|
|
{
|
|
order: 0,
|
|
offsetDays: 1,
|
|
tone: 'pas-un-ton',
|
|
subject: 'X',
|
|
body: 'X',
|
|
requiresManualValidation: false,
|
|
},
|
|
],
|
|
})
|
|
r.assertStatus(422)
|
|
})
|
|
|
|
test("cross-org : chaque org édite SA copie, pas celle de l'autre", async ({
|
|
client,
|
|
assert,
|
|
}) => {
|
|
const { a, b } = await createTwoOrgs()
|
|
|
|
const r = await client
|
|
.patch('/api/v1/plans/standard-30j')
|
|
.headers(b.bearer)
|
|
.json({ name: 'Renommé par B' })
|
|
r.assertStatus(200)
|
|
|
|
const fromA = await client.get('/api/v1/plans/standard-30j').headers(a.bearer)
|
|
assert.equal(body<ApiOk<PlanShape>>(fromA).data.name, 'Standard B2B')
|
|
})
|
|
})
|