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>
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
/**
|
|
* Fake repos pour les tests routes : implementent l'API publique des repos
|
|
* (list/get/create/update*) en utilisant un store in-memory.
|
|
*/
|
|
|
|
import type { Decimal } from 'decimal.js';
|
|
import type { Attribution } from '../../src/domain/attribution.js';
|
|
import type { Bloc } from '../../src/domain/bloc.js';
|
|
import type { Client } from '../../src/domain/client.js';
|
|
import type { Formation } from '../../src/domain/formation.js';
|
|
import type { Intervention } from '../../src/domain/intervention.js';
|
|
import type { Module } from '../../src/domain/module.js';
|
|
import type { Personne } from '../../src/domain/personne.js';
|
|
import type { Projet } from '../../src/domain/projet.js';
|
|
import type { Tache } from '../../src/domain/tache.js';
|
|
import type { StatutAttribution, StatutIntervention } from '../../src/domain/types.js';
|
|
import { errors } from '../../src/lib/errors.js';
|
|
import type { RepoSet } from '../../src/repos/baserow-repo.js';
|
|
|
|
interface ListResult<T> {
|
|
items: T[];
|
|
meta: { page: number; per_page: number; total: number; total_pages: number };
|
|
}
|
|
|
|
class FakeReadRepo<T extends { id: number }> {
|
|
constructor(
|
|
private readonly entityName: string,
|
|
public store: T[] = [],
|
|
) {}
|
|
|
|
list(): Promise<ListResult<T>> {
|
|
return Promise.resolve({
|
|
items: this.store,
|
|
meta: { page: 1, per_page: 200, total: this.store.length, total_pages: 1 },
|
|
});
|
|
}
|
|
|
|
get(id: number): Promise<T> {
|
|
const found = this.store.find((x) => x.id === id);
|
|
if (!found) return Promise.reject(errors.notFound(this.entityName, id));
|
|
return Promise.resolve(found);
|
|
}
|
|
}
|
|
|
|
export class FakeAttributionRepo extends FakeReadRepo<Attribution> {
|
|
public lastCreated?: {
|
|
moduleId: number;
|
|
personneId: number;
|
|
heuresAttribuees: Decimal;
|
|
statut: StatutAttribution;
|
|
};
|
|
public lastUpdate?: { id: number; heures: Decimal };
|
|
public nextId = 1000;
|
|
|
|
constructor(store: Attribution[] = []) {
|
|
super('Attribution', store);
|
|
}
|
|
|
|
create(input: {
|
|
moduleId: number;
|
|
personneId: number;
|
|
heuresAttribuees: Decimal;
|
|
dateDebut: Date | null;
|
|
dateFin: Date | null;
|
|
statut: StatutAttribution;
|
|
}) {
|
|
this.lastCreated = input;
|
|
const id = this.nextId++;
|
|
return Promise.resolve({ id, order: '1', ...input });
|
|
}
|
|
|
|
updateHeuresRealisees(id: number, heures: Decimal) {
|
|
this.lastUpdate = { id, heures };
|
|
return Promise.resolve({ id, order: '1', attribution_heures_realisees: heures.toNumber() });
|
|
}
|
|
}
|
|
|
|
export class FakeInterventionRepo extends FakeReadRepo<Intervention> {
|
|
public lastCreated?: {
|
|
tacheId: number;
|
|
personneId: number;
|
|
heures: Decimal;
|
|
date: Date;
|
|
notes: string | null;
|
|
statut: StatutIntervention;
|
|
};
|
|
public nextId = 2000;
|
|
|
|
constructor(store: Intervention[] = []) {
|
|
super('Intervention', store);
|
|
}
|
|
|
|
create(input: {
|
|
tacheId: number;
|
|
personneId: number;
|
|
heures: Decimal;
|
|
date: Date;
|
|
notes: string | null;
|
|
statut: StatutIntervention;
|
|
}) {
|
|
this.lastCreated = input;
|
|
const id = this.nextId++;
|
|
return Promise.resolve({ id, order: '1', ...input });
|
|
}
|
|
}
|
|
|
|
export interface FakeReposBundle extends RepoSet {
|
|
personnes: FakeReadRepo<Personne> & RepoSet['personnes'];
|
|
formations: FakeReadRepo<Formation> & RepoSet['formations'];
|
|
blocs: FakeReadRepo<Bloc> & RepoSet['blocs'];
|
|
modules: FakeReadRepo<Module> & RepoSet['modules'];
|
|
attributions: FakeAttributionRepo & RepoSet['attributions'];
|
|
clients: FakeReadRepo<Client> & RepoSet['clients'];
|
|
projets: FakeReadRepo<Projet> & RepoSet['projets'];
|
|
taches: FakeReadRepo<Tache> & RepoSet['taches'];
|
|
interventions: FakeInterventionRepo & RepoSet['interventions'];
|
|
}
|
|
|
|
export function buildFakeRepos(stores: {
|
|
personnes?: Personne[];
|
|
formations?: Formation[];
|
|
blocs?: Bloc[];
|
|
modules?: Module[];
|
|
attributions?: Attribution[];
|
|
clients?: Client[];
|
|
projets?: Projet[];
|
|
taches?: Tache[];
|
|
interventions?: Intervention[];
|
|
}): FakeReposBundle {
|
|
// Cast force : on shippe l'interface publique RepoSet meme si les classes
|
|
// BaseRepo ne sont pas etendues. Les tests ne tapent que les methodes utilisees.
|
|
return {
|
|
personnes: new FakeReadRepo<Personne>('Personne', stores.personnes ?? []),
|
|
formations: new FakeReadRepo<Formation>('Formation', stores.formations ?? []),
|
|
blocs: new FakeReadRepo<Bloc>('Bloc', stores.blocs ?? []),
|
|
modules: new FakeReadRepo<Module>('Module', stores.modules ?? []),
|
|
attributions: new FakeAttributionRepo(stores.attributions ?? []),
|
|
clients: new FakeReadRepo<Client>('Client', stores.clients ?? []),
|
|
projets: new FakeReadRepo<Projet>('Projet', stores.projets ?? []),
|
|
taches: new FakeReadRepo<Tache>('Tache', stores.taches ?? []),
|
|
interventions: new FakeInterventionRepo(stores.interventions ?? []),
|
|
} as unknown as FakeReposBundle;
|
|
}
|