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>
223 lines
6.3 KiB
TypeScript
223 lines
6.3 KiB
TypeScript
/**
|
|
* Tests des mappers Row -> Domain. On instancie les repos avec un BaserowClient mock
|
|
* minimal qui rend juste getRow/listRows.
|
|
*/
|
|
|
|
import type { Logger } from 'pino';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import type {
|
|
BaserowClient,
|
|
BaserowPaginatedResponse,
|
|
BaserowRow,
|
|
} from '../../src/adapters/baserow-client.js';
|
|
import { logger } from '../../src/lib/logger.js';
|
|
import {
|
|
AttributionRepo,
|
|
FormationRepo,
|
|
ModuleRepo,
|
|
PersonneRepo,
|
|
ProjetRepo,
|
|
} from '../../src/repos/baserow-repo.js';
|
|
|
|
function fakeClient(rowsByTable: Record<number, BaserowRow[]>): BaserowClient {
|
|
return {
|
|
listRows: vi.fn(
|
|
(tableId: number): Promise<BaserowPaginatedResponse> =>
|
|
Promise.resolve({
|
|
count: rowsByTable[tableId]?.length ?? 0,
|
|
next: null,
|
|
previous: null,
|
|
results: rowsByTable[tableId] ?? [],
|
|
}),
|
|
),
|
|
getRow: vi.fn((tableId: number, rowId: number): Promise<BaserowRow> => {
|
|
const row = (rowsByTable[tableId] ?? []).find((r) => r.id === rowId);
|
|
if (!row) return Promise.reject(Object.assign(new Error('not found'), { code: 'NOT_FOUND' }));
|
|
return Promise.resolve(row);
|
|
}),
|
|
createRow: vi.fn(),
|
|
updateRow: vi.fn(),
|
|
deleteRow: vi.fn(),
|
|
resolveTableIds: vi.fn(),
|
|
healthCheck: vi.fn(),
|
|
} as unknown as BaserowClient;
|
|
}
|
|
|
|
const log = logger as Logger;
|
|
|
|
describe('PersonneRepo', () => {
|
|
it('mappe une row Baserow vers Personne', async () => {
|
|
const row: BaserowRow = {
|
|
id: 42,
|
|
order: '1',
|
|
personne_nom: 'Dupont',
|
|
personne_prenom: 'Pierre',
|
|
personne_email: 'p@a.fr',
|
|
personne_capacite_annuelle: '1000',
|
|
personne_split_formation_pct: 60,
|
|
personne_split_agence_pct: 40,
|
|
personne_roles: [{ id: 1, value: 'formateur', color: 'blue' }],
|
|
personne_statut: { id: 2, value: 'actif', color: 'green' },
|
|
personne_heures_attribuees_formation: '0',
|
|
personne_heures_attribuees_agence: '0',
|
|
};
|
|
const repo = new PersonneRepo({
|
|
client: fakeClient({ 1: [row] }),
|
|
tableId: 1,
|
|
entityName: 'Personne',
|
|
logger: log,
|
|
});
|
|
const personne = await repo.get(42);
|
|
expect(personne.id).toBe(42);
|
|
expect(personne.nom).toBe('Dupont');
|
|
expect(personne.hasRole('formateur')).toBe(true);
|
|
expect(personne.statut).toBe('actif');
|
|
expect(personne.capaciteAnnuelle.toNumber()).toBe(1000);
|
|
});
|
|
|
|
it('list pagine et map', async () => {
|
|
const repo = new PersonneRepo({
|
|
client: fakeClient({
|
|
1: [
|
|
{
|
|
id: 1,
|
|
order: '1',
|
|
personne_nom: 'A',
|
|
personne_prenom: 'B',
|
|
personne_email: 'a@b.c',
|
|
personne_capacite_annuelle: '1',
|
|
personne_split_formation_pct: 50,
|
|
personne_split_agence_pct: 50,
|
|
personne_roles: [],
|
|
personne_statut: 'actif',
|
|
},
|
|
],
|
|
}),
|
|
tableId: 1,
|
|
entityName: 'Personne',
|
|
logger: log,
|
|
});
|
|
const res = await repo.list({ size: 50 });
|
|
expect(res.items).toHaveLength(1);
|
|
expect(res.meta.total).toBe(1);
|
|
});
|
|
|
|
it('throw NOT_FOUND si row inexistante', async () => {
|
|
const repo = new PersonneRepo({
|
|
client: fakeClient({ 1: [] }),
|
|
tableId: 1,
|
|
entityName: 'Personne',
|
|
logger: log,
|
|
});
|
|
await expect(repo.get(999)).rejects.toMatchObject({ code: 'NOT_FOUND' });
|
|
});
|
|
});
|
|
|
|
describe('FormationRepo', () => {
|
|
it('mappe filiere/statut select', async () => {
|
|
const row: BaserowRow = {
|
|
id: 10,
|
|
order: '1',
|
|
formation_nom: 'Dev',
|
|
formation_filiere: { id: 1, value: 'dev', color: 'blue' },
|
|
formation_heures_totales: 500,
|
|
formation_statut: { id: 2, value: 'actif', color: 'green' },
|
|
};
|
|
const repo = new FormationRepo({
|
|
client: fakeClient({ 2: [row] }),
|
|
tableId: 2,
|
|
entityName: 'Formation',
|
|
logger: log,
|
|
});
|
|
const f = await repo.get(10);
|
|
expect(f.filiere).toBe('dev');
|
|
expect(f.statut).toBe('actif');
|
|
});
|
|
});
|
|
|
|
describe('ModuleRepo', () => {
|
|
it('mappe blocId via link field', async () => {
|
|
const row: BaserowRow = {
|
|
id: 200,
|
|
order: '1',
|
|
module_nom: 'JS',
|
|
module_heures_prevues: 30,
|
|
module_statut: 'a_attribuer',
|
|
module_bloc: [{ id: 100, value: 'Bloc JS' }],
|
|
};
|
|
const repo = new ModuleRepo({
|
|
client: fakeClient({ 4: [row] }),
|
|
tableId: 4,
|
|
entityName: 'Module',
|
|
logger: log,
|
|
});
|
|
const m = await repo.get(200);
|
|
expect(m.blocId).toBe(100);
|
|
expect(m.statut).toBe('a_attribuer');
|
|
});
|
|
});
|
|
|
|
describe('AttributionRepo', () => {
|
|
it('mappe + create persiste les bons fields', async () => {
|
|
const row: BaserowRow = {
|
|
id: 500,
|
|
order: '1',
|
|
attribution_heures_attribuees: 10,
|
|
attribution_heures_realisees: 0,
|
|
attribution_module: [{ id: 200, value: 'JS' }],
|
|
attribution_personne: [{ id: 1, value: 'Pierre' }],
|
|
attribution_statut: 'planifie',
|
|
};
|
|
const client = fakeClient({ 5: [row] });
|
|
const repo = new AttributionRepo({
|
|
client,
|
|
tableId: 5,
|
|
entityName: 'Attribution',
|
|
logger: log,
|
|
});
|
|
const a = await repo.get(500);
|
|
expect(a.moduleId).toBe(200);
|
|
expect(a.personneId).toBe(1);
|
|
|
|
await repo.create({
|
|
moduleId: 200,
|
|
personneId: 1,
|
|
heuresAttribuees: a.heuresAttribuees,
|
|
dateDebut: new Date('2026-09-01'),
|
|
dateFin: null,
|
|
statut: 'planifie',
|
|
});
|
|
expect(client.createRow).toHaveBeenCalledWith(
|
|
5,
|
|
expect.objectContaining({
|
|
attribution_module: [200],
|
|
attribution_personne: [1],
|
|
attribution_statut: 'planifie',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ProjetRepo', () => {
|
|
it('mappe statut + clientId', async () => {
|
|
const row: BaserowRow = {
|
|
id: 300,
|
|
order: '1',
|
|
projet_nom: 'Acme',
|
|
projet_charge_heures: 80,
|
|
projet_client: [{ id: 50, value: 'Acme Inc' }],
|
|
projet_statut: { id: 1, value: 'en_cours', color: 'blue' },
|
|
projet_type: { id: 2, value: 'site_web', color: 'blue' },
|
|
};
|
|
const repo = new ProjetRepo({
|
|
client: fakeClient({ 7: [row] }),
|
|
tableId: 7,
|
|
entityName: 'Projet',
|
|
logger: log,
|
|
});
|
|
const p = await repo.get(300);
|
|
expect(p.clientId).toBe(50);
|
|
expect(p.statut).toBe('en_cours');
|
|
expect(p.type).toBe('site_web');
|
|
});
|
|
});
|