Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
Wiring HTTP du bridge service. 10 endpoints livres (cf docs/19 §6.1-6.5) :
- GET /api/v1/personnes (+ /:id, + /:id/dashboard)
- GET /api/v1/formations (+ /:id avec rollups blocs/modules)
- GET /api/v1/projets (+ /:id avec rollups taches)
- POST /api/v1/modules/:id/attribuer (RG-01 -> 422, role/heures invalides -> 400)
- POST /api/v1/interventions (validation role developpeur + heures > 0)
- PATCH /api/v1/attributions/:id/heures-realisees (409 si annule/realise)
Layers ajoutees :
- src/middleware/auth.ts : Bearer brg_*, scopes JSON-encoded BRIDGE_API_TOKENS, admin:* wildcard
- src/middleware/error-handler.ts : BridgeError -> JSON shape standard
- src/lib/container.ts : DI singleton (Baserow + Redis + 9 repos), setContainer testable
- src/lib/http.ts : parseListQuery + parseBody zod helper
- src/repos/baserow-repo.ts : BaseRepo<T> abstrait + 9 sous-classes (mapping Row<->Domain)
- src/routes/{personnes,formations,projets,modules,interventions,attributions}.ts
src/index.ts reecrit : buildApp() + initContainer + auth sur /api/v1/* + ready check Baserow+Redis.
Tests : 163/163 verts (12 suites domain + 8 nouvelles : auth, repos, 6 routes).
Coverage src global : 70.77% (cible 60%). Domain 97.86%, routes 96%, middleware 86%.
Choix : BaseRepo abstrait (pas mega-generic, Ockham) ; FakeRepos in-memory pour tests routes
(pas de testcontainers ici, c'est Bloc 7) ; mapping erreurs domain -> HTTP par message texte
(fragile, sera refactor en DomainError typees au Bloc 3.2).
Hors scope (a venir) :
- Bloc 5 : rate limiting Redis
- Bloc 7 : webhook handlers Baserow + sync bidirec + cache invalidation
- Bloc 3.2 : routes /docmost/*, /sync/*, /rapports/*
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.5 KiB
TypeScript
100 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { buildFakeRepos } from '../helpers/fake-repos.js';
|
|
import { makeAttribution, makeModule, makePersonne } from '../helpers/fixtures.js';
|
|
import {
|
|
READ_ALL_TOKEN,
|
|
WRITE_ALL_TOKEN,
|
|
buildTestApp,
|
|
installTestContainer,
|
|
resetTestContainer,
|
|
} from '../helpers/test-app.js';
|
|
|
|
function postAttribuer(
|
|
app: ReturnType<typeof buildTestApp>,
|
|
body: Record<string, unknown>,
|
|
token = WRITE_ALL_TOKEN,
|
|
) {
|
|
return app.request('/api/v1/modules/200/attribuer', {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
describe('POST /api/v1/modules/:id/attribuer', () => {
|
|
afterEach(resetTestContainer);
|
|
|
|
it('happy path 201 + persistance via repo', async () => {
|
|
const repos = buildFakeRepos({
|
|
personnes: [makePersonne({ id: 1, roles: ['formateur'] })],
|
|
modules: [makeModule(200, 100)],
|
|
});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
|
|
const res = await postAttribuer(app, {
|
|
personne_id: 1,
|
|
heures: 10,
|
|
date_debut: '2026-09-01T00:00:00Z',
|
|
});
|
|
expect(res.status).toBe(201);
|
|
const body = (await res.json()) as { data: { attribution_id: number; statut: string } };
|
|
expect(body.data.statut).toBe('planifie');
|
|
expect(repos.attributions.lastCreated?.heuresAttribuees.toNumber()).toBe(10);
|
|
});
|
|
|
|
it('422 RG-01 si heures > capacite module', async () => {
|
|
const repos = buildFakeRepos({
|
|
personnes: [makePersonne({ id: 1, roles: ['formateur'] })],
|
|
modules: [makeModule(200, 100)], // heuresPrevues = 30
|
|
attributions: [makeAttribution({ id: 500, moduleId: 200, personneId: 99 })], // 10h deja
|
|
});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
|
|
const res = await postAttribuer(app, { personne_id: 1, heures: 50 });
|
|
expect(res.status).toBe(422);
|
|
const body = (await res.json()) as { error: { code: string; details: { rule: string } } };
|
|
expect(body.error.code).toBe('RG_VIOLATION');
|
|
expect(body.error.details.rule).toBe('RG-01');
|
|
});
|
|
|
|
it('422 (validation) si role formateur manquant', async () => {
|
|
const repos = buildFakeRepos({
|
|
personnes: [makePersonne({ id: 1, roles: ['developpeur'] })],
|
|
modules: [makeModule(200, 100)],
|
|
});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
|
|
const res = await postAttribuer(app, { personne_id: 1, heures: 5 });
|
|
expect(res.status).toBe(400);
|
|
const body = (await res.json()) as { error: { code: string } };
|
|
expect(body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
it('400 si body invalide (heures negatives)', async () => {
|
|
const repos = buildFakeRepos({});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
const res = await postAttribuer(app, { personne_id: 1, heures: -3 });
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('401 sans token', async () => {
|
|
const repos = buildFakeRepos({});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
const res = await app.request('/api/v1/modules/200/attribuer', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ personne_id: 1, heures: 5 }),
|
|
});
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it('403 avec mauvais scope', async () => {
|
|
const repos = buildFakeRepos({});
|
|
const app = buildTestApp(installTestContainer({ repos }));
|
|
const res = await postAttribuer(app, { personne_id: 1, heures: 5 }, READ_ALL_TOKEN);
|
|
expect(res.status).toBe(403);
|
|
});
|
|
});
|