mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +00:00
- migration r1_referentiel : Category (EQUIPMENT/COMPONENT_TYPE), Location (site → zone, lat/lng + colonne PostGIS générée geography(Point,4326) + index GIST), Asset (statut d'équipement), AssetComponent (organe sans emplacement PAR CONSTRUCTION), Team, invitation sur User ; migration autosuffisante (CREATE EXTENSION IF NOT EXISTS postgis) - contrat : 21 nouvelles opérations (26 total), générateur OpenAPI étendu aux paramètres de chemin ; spec + client web régénérés dans ce commit - API : modules categories/locations/assets/teams + gestion des personnes (liste, rôles, invitation lien 7 j à usage unique, activation publique qui connecte directement, mise à jour rôle/équipes) — tout sous @RequirePermission ; invariants en service (profondeur 2, kinds, catégorie jamais supprimée) - seed : parc de la maquette validée (5 sites + 8 zones, 8 appareils, organes A1/B2, 9 catégories, 2 équipes) — idempotent - 36 tests verts (couverture 96 % stmts / 85 % branches) : recette site→zone→appareil→organes, matrice vivante, invitation→activation ; smoke test sur build de prod - CI : postgres → postgis/postgis:18-3.6 (la migration R1 l'exige) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
222 lines
7.4 KiB
TypeScript
222 lines
7.4 KiB
TypeScript
/**
|
|
* E2E R1 — administration du référentiel : équipes, renommages, mises à jour
|
|
* et TOUS les refus (conflits 409, inconnus 404, invalides 400) — le contrat
|
|
* d'erreur fait partie du contrat.
|
|
*/
|
|
process.env.DEMO_MODE = 'true';
|
|
|
|
import { INestApplication } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
import { PrismaClient } from '@prisma/client';
|
|
import request from 'supertest';
|
|
import { seed } from '../prisma/seed';
|
|
import { AppModule } from '../src/app.module';
|
|
|
|
const GHOST = '00000000-0000-4000-8000-000000000000';
|
|
|
|
describe('Administration du référentiel (e2e)', () => {
|
|
let app: INestApplication;
|
|
let admin: string;
|
|
const prisma = new PrismaClient();
|
|
const http = () => request(app.getHttpServer());
|
|
const suffix = `adm-${Date.now().toString(36)}`;
|
|
|
|
beforeAll(async () => {
|
|
await seed(prisma);
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [AppModule.forRoot()],
|
|
}).compile();
|
|
app = moduleRef.createNestApplication();
|
|
await app.init();
|
|
const { body } = await http().get('/auth/demo-accounts');
|
|
const compte = body.accounts.find(
|
|
(a: { roleName: string }) => a.roleName === 'Administrateur',
|
|
);
|
|
admin = (await http().post('/auth/demo-login').send({ userId: compte.id })).body
|
|
.accessToken;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.team.deleteMany({ where: { name: { contains: suffix } } });
|
|
await prisma.category.deleteMany({ where: { name: { contains: suffix } } });
|
|
await prisma.location.deleteMany({ where: { name: { contains: suffix } } });
|
|
await app?.close();
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
const auth = () => ({ Authorization: `Bearer ${admin}` });
|
|
|
|
it('équipes : création, doublon 409, membres, 404', async () => {
|
|
const { body: users } = await http().get('/users').set(auth());
|
|
const ahmed = users.users.find(
|
|
(u: { email: string }) => u.email === 'technicien@demo.siop.ma',
|
|
);
|
|
|
|
const team = await http()
|
|
.post('/teams')
|
|
.set(auth())
|
|
.send({ name: `Équipe ${suffix}`, description: 'Zone de test' })
|
|
.expect(201);
|
|
expect(team.body.members).toHaveLength(0);
|
|
|
|
await http()
|
|
.post('/teams')
|
|
.set(auth())
|
|
.send({ name: `Équipe ${suffix}` })
|
|
.expect(409);
|
|
|
|
const updated = await http()
|
|
.patch(`/teams/${team.body.id}`)
|
|
.set(auth())
|
|
.send({ memberIds: [ahmed.id], description: 'Zone mise à jour' })
|
|
.expect(200);
|
|
expect(updated.body.members.map((m: { id: string }) => m.id)).toEqual([ahmed.id]);
|
|
|
|
const { body: teams } = await http().get('/teams').set(auth()).expect(200);
|
|
expect(teams.teams.some((t: { id: string }) => t.id === team.body.id)).toBe(true);
|
|
|
|
await http().patch(`/teams/${GHOST}`).set(auth()).send({ name: 'X' }).expect(404);
|
|
await http()
|
|
.patch(`/teams/${team.body.id}`)
|
|
.set(auth())
|
|
.send({ name: 'Casablanca Centre' })
|
|
.expect(409);
|
|
});
|
|
|
|
it('catégories : création, doublon 409, renommage, 404', async () => {
|
|
const created = await http()
|
|
.post('/categories')
|
|
.set(auth())
|
|
.send({ kind: 'EQUIPMENT', name: `Catégorie ${suffix}` })
|
|
.expect(201);
|
|
await http()
|
|
.post('/categories')
|
|
.set(auth())
|
|
.send({ kind: 'EQUIPMENT', name: `Catégorie ${suffix}` })
|
|
.expect(409);
|
|
const renamed = await http()
|
|
.patch(`/categories/${created.body.id}`)
|
|
.set(auth())
|
|
.send({ name: `Catégorie ${suffix} v2` })
|
|
.expect(200);
|
|
expect(renamed.body.name).toBe(`Catégorie ${suffix} v2`);
|
|
await http()
|
|
.patch(`/categories/${GHOST}`)
|
|
.set(auth())
|
|
.send({ name: 'X' })
|
|
.expect(404);
|
|
});
|
|
|
|
it('emplacements : renommage, parent inconnu, auto-parent, site avec zones, 404', async () => {
|
|
const site = await http()
|
|
.post('/locations')
|
|
.set(auth())
|
|
.send({ name: `Site ${suffix}` })
|
|
.expect(201);
|
|
const zone = await http()
|
|
.post('/locations')
|
|
.set(auth())
|
|
.send({ name: `Zone ${suffix}`, parentId: site.body.id })
|
|
.expect(201);
|
|
|
|
await http()
|
|
.patch(`/locations/${zone.body.id}`)
|
|
.set(auth())
|
|
.send({ guardianName: 'Gardien Test', guardianPhone: '06 00 00 00 00' })
|
|
.expect(200);
|
|
|
|
await http()
|
|
.post('/locations')
|
|
.set(auth())
|
|
.send({ name: `Orphelin ${suffix}`, parentId: GHOST })
|
|
.expect(400);
|
|
await http()
|
|
.patch(`/locations/${zone.body.id}`)
|
|
.set(auth())
|
|
.send({ parentId: zone.body.id })
|
|
.expect(400);
|
|
// Le site a une zone : il ne peut pas devenir zone lui-même
|
|
await http()
|
|
.patch(`/locations/${site.body.id}`)
|
|
.set(auth())
|
|
.send({ parentId: zone.body.id })
|
|
.expect(400);
|
|
await http().patch(`/locations/${GHOST}`).set(auth()).send({ name: 'X' }).expect(404);
|
|
});
|
|
|
|
it('appareils : statut, référence en conflit, emplacement inconnu, 404', async () => {
|
|
const { body } = await http().get('/assets').set(auth());
|
|
const a1 = body.assets.find((a: { reference: string }) => a.reference === 'A1');
|
|
const b1 = body.assets.find((a: { reference: string }) => a.reference === 'B1');
|
|
|
|
const updated = await http()
|
|
.patch(`/assets/${b1.id}`)
|
|
.set(auth())
|
|
.send({ status: 'UNDER_MAINTENANCE' })
|
|
.expect(200);
|
|
expect(updated.body.status).toBe('UNDER_MAINTENANCE');
|
|
await http()
|
|
.patch(`/assets/${b1.id}`)
|
|
.set(auth())
|
|
.send({ status: 'IN_SERVICE' })
|
|
.expect(200);
|
|
|
|
await http()
|
|
.patch(`/assets/${b1.id}`)
|
|
.set(auth())
|
|
.send({ reference: a1.reference })
|
|
.expect(409);
|
|
await http()
|
|
.patch(`/assets/${b1.id}`)
|
|
.set(auth())
|
|
.send({ locationId: GHOST })
|
|
.expect(400);
|
|
await http().get(`/assets/${GHOST}`).set(auth()).expect(404);
|
|
await http()
|
|
.patch(`/assets/${GHOST}`)
|
|
.set(auth())
|
|
.send({ brand: 'X' })
|
|
.expect(404);
|
|
await http()
|
|
.post(`/assets/${GHOST}/components`)
|
|
.set(auth())
|
|
.send({ typeId: GHOST })
|
|
.expect(404);
|
|
await http()
|
|
.delete(`/assets/${b1.id}/components/${GHOST}`)
|
|
.set(auth())
|
|
.expect(404);
|
|
});
|
|
|
|
it('personnes : mise à jour rôle/équipes, invitation sur rôle inconnu, 404', async () => {
|
|
const { body: users } = await http().get('/users').set(auth());
|
|
const youssef = users.users.find(
|
|
(u: { email: string }) => u.email === 'technicien-limite@demo.siop.ma',
|
|
);
|
|
const { body: teams } = await http().get('/teams').set(auth());
|
|
const mohammedia = teams.teams.find((t: { name: string }) => t.name === 'Mohammedia');
|
|
|
|
const updated = await http()
|
|
.patch(`/users/${youssef.id}`)
|
|
.set(auth())
|
|
.send({ phone: '06 12 34 56 78', teamIds: [mohammedia.id] })
|
|
.expect(200);
|
|
expect(updated.body.teams.map((t: { name: string }) => t.name)).toContain('Mohammedia');
|
|
// remise en l'état (seed idempotent : les équipes ne sont pas réécrites)
|
|
const casa = teams.teams.find((t: { name: string }) => t.name === 'Casablanca Centre');
|
|
await http()
|
|
.patch(`/users/${youssef.id}`)
|
|
.set(auth())
|
|
.send({ teamIds: [casa.id] })
|
|
.expect(200);
|
|
|
|
await http().patch(`/users/${GHOST}`).set(auth()).send({ phone: '0' }).expect(404);
|
|
await http()
|
|
.post('/users/invitations')
|
|
.set(auth())
|
|
.send({ email: `x-${suffix}@spelev.ma`, displayName: 'X', roleId: GHOST })
|
|
.expect(404);
|
|
await http().post(`/users/${GHOST}/invitation`).set(auth()).expect(404);
|
|
});
|
|
});
|