Wiki/bridge/tests/routes/attributions.test.ts
Corentin JOGUET c8e9b4d4ea
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
feat(bridge): bloc 3 — routes REST Tier 1 + auth + repos Baserow (10 endpoints)
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>
2026-05-07 20:01:36 +02:00

74 lines
2.5 KiB
TypeScript

import { Decimal } from 'decimal.js';
import { afterEach, describe, expect, it } from 'vitest';
import { Attribution } from '../../src/domain/attribution.js';
import { buildFakeRepos } from '../helpers/fake-repos.js';
import { makeAttribution } from '../helpers/fixtures.js';
import {
WRITE_ALL_TOKEN,
buildTestApp,
installTestContainer,
resetTestContainer,
} from '../helpers/test-app.js';
function patchHeures(
app: ReturnType<typeof buildTestApp>,
id: number,
body: Record<string, unknown>,
) {
return app.request(`/api/v1/attributions/${id}/heures-realisees`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${WRITE_ALL_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
}
describe('PATCH /api/v1/attributions/:id/heures-realisees', () => {
afterEach(resetTestContainer);
it('happy path 200', async () => {
const attrib = makeAttribution({ id: 500 });
const repos = buildFakeRepos({ attributions: [attrib] });
const app = buildTestApp(installTestContainer({ repos }));
const res = await patchHeures(app, 500, { heures_realisees: 4.5, comment: 'sprint 1' });
expect(res.status).toBe(200);
const body = (await res.json()) as { data: { heures_realisees: string } };
expect(body.data.heures_realisees).toBe('4.50');
expect(repos.attributions.lastUpdate?.id).toBe(500);
expect(repos.attributions.lastUpdate?.heures.toNumber()).toBe(4.5);
});
it('409 si attribution annulee', async () => {
const annulee = new Attribution({
id: 500,
moduleId: 200,
personneId: 1,
heuresAttribuees: new Decimal(10),
statut: 'annule',
});
const repos = buildFakeRepos({ attributions: [annulee] });
const app = buildTestApp(installTestContainer({ repos }));
const res = await patchHeures(app, 500, { heures_realisees: 2 });
expect(res.status).toBe(409);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('CONFLICT');
});
it('404 si attribution inconnue', async () => {
const repos = buildFakeRepos({});
const app = buildTestApp(installTestContainer({ repos }));
const res = await patchHeures(app, 9999, { heures_realisees: 1 });
expect(res.status).toBe(404);
});
it('400 si heures negatives', async () => {
const repos = buildFakeRepos({ attributions: [makeAttribution({ id: 500 })] });
const app = buildTestApp(installTestContainer({ repos }));
const res = await patchHeures(app, 500, { heures_realisees: -1 });
expect(res.status).toBe(400);
});
});