mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r4.3): file d'écriture, verrou optimiste D2, Synchro & conflits
Serveur : updatedAt exposé au détail OT ; baseUpdatedAt optionnel sur transition/coche/bilan → 409 « Conflit de version » contextualisé (qui, quand) ; toute écriture secondaire (coche, bilan, commentaire, conso, MO) fait avancer la version — sans version fournie, le web est inchangé. ADR-003 : sécurité & protocole de routage mobile (qui vit où sur l'appareil, purge complète à la déconnexion — correctif réel : la file et le cache persisté survivaient au logout). Mobile : file persistée AsyncStorage rejouée dans l'ordre — succès → propagation de la version fraîche aux saisies restantes du même OT (nos écritures ne se conflictent pas entre elles, un écart étranger reste détecté) ; coupure → tout attend ; refus → CONFLIT, la file s'arrête, l'humain tranche (voir l'OT / rejouer sur version à jour / abandonner). Transitions, coches, bilan et photos (D5, compressées ~1600 px) passent par la file avec patch optimiste du cache ; écran Synchro (badge tabbar ambre/rouge) ; préchargement parc + référentiels (le bilan hors-ligne a ses vocabulaires). Recette « mode avion » 13/13 en Expo web piloté : gestes hors-ligne → 3 en file → modification concurrente de Salma → conflit tranché → serveur Terminé avec bilan. 17 tests jest-expo, 74 tests API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,3 +139,109 @@ describe('Recherche globale & corrections de recette R3 (e2e)', () => {
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Verrou optimiste D2 (e2e) — la version protège les saisies mobiles', () => {
|
||||
let app: INestApplication;
|
||||
let salma: string;
|
||||
let ahmed: string;
|
||||
let otId: string;
|
||||
const prisma = new PrismaClient();
|
||||
const http = () => request(app.getHttpServer());
|
||||
const auth = (t: string) => ({ Authorization: `Bearer ${t}` });
|
||||
const suffix = `verrou-${Date.now().toString(36)}`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule.forRoot()],
|
||||
}).compile();
|
||||
app = moduleRef.createNestApplication();
|
||||
await app.init();
|
||||
const { body } = await http().get('/auth/demo-accounts');
|
||||
const login = async (roleName: string) => {
|
||||
const compte = body.accounts.find((a: { roleName: string }) => a.roleName === roleName);
|
||||
return (await http().post('/auth/demo-login').send({ userId: compte.id })).body
|
||||
.accessToken as string;
|
||||
};
|
||||
salma = await login('Dispatcher');
|
||||
ahmed = await login('Technicien');
|
||||
const { body: assets } = await http().get('/assets').set(auth(salma));
|
||||
const a1 = assets.assets.find((a: { reference: string }) => a.reference === 'A1');
|
||||
const technicien = body.accounts.find((a: { roleName: string }) => a.roleName === 'Technicien');
|
||||
const cree = await http()
|
||||
.post('/work-orders')
|
||||
.set(auth(salma))
|
||||
.send({
|
||||
title: `Verrou E2E ${suffix}`,
|
||||
type: 'CORRECTIVE',
|
||||
assetId: a1.id,
|
||||
assigneeIds: [technicien.id],
|
||||
})
|
||||
.expect(201);
|
||||
otId = cree.body.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.workOrder.deleteMany({ where: { title: { contains: suffix } } });
|
||||
await app?.close();
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
it('version à jour : la transition passe ; le détail expose updatedAt', async () => {
|
||||
const detail = await http().get(`/work-orders/${otId}`).set(auth(ahmed)).expect(200);
|
||||
expect(detail.body.updatedAt).toBeTruthy();
|
||||
await http()
|
||||
.post(`/work-orders/${otId}/transition`)
|
||||
.set(auth(ahmed))
|
||||
.send({ to: 'IN_PROGRESS', baseUpdatedAt: detail.body.updatedAt })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('OT modifié entre-temps (commentaire) : 409 « Conflit de version » contextualisé', async () => {
|
||||
const detail = await http().get(`/work-orders/${otId}`).set(auth(ahmed)).expect(200);
|
||||
const versionLue = detail.body.updatedAt as string;
|
||||
// Salma commente pendant qu'Ahmed est « hors-ligne » — la version avance
|
||||
await http()
|
||||
.post(`/work-orders/${otId}/comments`)
|
||||
.set(auth(salma))
|
||||
.send({ message: 'Réassigné après appel du syndic' })
|
||||
.expect(201);
|
||||
const refus = await http()
|
||||
.post(`/work-orders/${otId}/transition`)
|
||||
.set(auth(ahmed))
|
||||
.send({ to: 'ON_HOLD', baseUpdatedAt: versionLue })
|
||||
.expect(409);
|
||||
expect(refus.body.message).toContain('Conflit de version');
|
||||
expect(refus.body.message).toContain('Salma');
|
||||
// L'OT n'a PAS bougé — rien d'écrasé en silence
|
||||
const apres = await http().get(`/work-orders/${otId}`).set(auth(ahmed)).expect(200);
|
||||
expect(apres.body.status).toBe('IN_PROGRESS');
|
||||
});
|
||||
|
||||
it('le bilan et la coche font avancer la version (sinon le verrou est aveugle)', async () => {
|
||||
const avant = (await http().get(`/work-orders/${otId}`).set(auth(ahmed))).body.updatedAt;
|
||||
const { body: refs } = await http().get('/reference-values').set(auth(ahmed));
|
||||
const porte = refs.referenceValues.find(
|
||||
(v: { field: string; isActive: boolean }) => v.field === 'DOOR_STATE' && v.isActive,
|
||||
);
|
||||
await http()
|
||||
.put(`/work-orders/${otId}/report`)
|
||||
.set(auth(ahmed))
|
||||
.send({ doorStateId: porte.id })
|
||||
.expect(200);
|
||||
const apres = (await http().get(`/work-orders/${otId}`).set(auth(ahmed))).body.updatedAt;
|
||||
expect(new Date(apres).getTime()).toBeGreaterThan(new Date(avant).getTime());
|
||||
});
|
||||
|
||||
it('sans baseUpdatedAt (web) : comportement inchangé, même après modification', async () => {
|
||||
await http()
|
||||
.post(`/work-orders/${otId}/comments`)
|
||||
.set(auth(salma))
|
||||
.send({ message: 'Nouvelle note' })
|
||||
.expect(201);
|
||||
await http()
|
||||
.post(`/work-orders/${otId}/transition`)
|
||||
.set(auth(ahmed))
|
||||
.send({ to: 'ON_HOLD' })
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user