ci(r0.12): pipeline GitHub Actions — ci-contract, api (couverture ≥ 70 %), web

- ci-contract : régénère docs/openapi.json + schema.d.ts et échoue au
  moindre diff — la règle d'or (ADR-001) devient bloquante
- api : services PostgreSQL 18 + Redis, prisma migrate deploy, typecheck,
  Jest avec coverageThreshold global 70 % (mesuré : 97,5 % stmts / 90,7 % branches)
- web : typecheck + vitest + build de production
- test unitaire PermissionsGuard (23 tests au total) ; badge CI au README

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-15 22:24:59 +01:00
parent 4f3af5af4f
commit 83efc33f0f
5 changed files with 176 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import { ForbiddenException } from '@nestjs/common';
import type { ExecutionContext } from '@nestjs/common';
import type { Reflector } from '@nestjs/core';
import { PermissionsGuard } from './permissions.guard';
import type { PermissionsService } from './permissions.service';
import type { RequiredPermission } from './require-permission.decorator';
function contextWithUser(user?: { userId: string; roleId: string }) {
return {
getHandler: () => ({}),
getClass: () => ({}),
switchToHttp: () => ({ getRequest: () => ({ user }) }),
} as unknown as ExecutionContext;
}
describe('PermissionsGuard (matrice relue en base — le JWT ne porte aucun droit)', () => {
const reflector = (required?: RequiredPermission) =>
({ getAllAndOverride: () => required }) as unknown as Reflector;
const service = (allowed: boolean) =>
({ can: jest.fn().mockResolvedValue(allowed) }) as unknown as PermissionsService;
it('laisse passer une route sans @RequirePermission', async () => {
const guard = new PermissionsGuard(reflector(undefined), service(false));
await expect(guard.canActivate(contextWithUser())).resolves.toBe(true);
});
it('refuse si aucun utilisateur authentifié ne porte la requête', async () => {
const guard = new PermissionsGuard(
reflector({ category: 'WORK_ORDERS', right: 'view' }),
service(true),
);
await expect(guard.canActivate(contextWithUser(undefined))).rejects.toThrow(
ForbiddenException,
);
});
it('autorise quand la matrice accorde le droit', async () => {
const permissions = service(true);
const guard = new PermissionsGuard(
reflector({ category: 'WORK_ORDERS', right: 'create' }),
permissions,
);
await expect(
guard.canActivate(contextWithUser({ userId: 'u1', roleId: 'r1' })),
).resolves.toBe(true);
expect(permissions.can).toHaveBeenCalledWith('r1', 'WORK_ORDERS', 'create');
});
it('refuse (403, droit nommé) quand la matrice ne laccorde pas', async () => {
const guard = new PermissionsGuard(
reflector({ category: 'SETTINGS', right: 'delete' }),
service(false),
);
await expect(
guard.canActivate(contextWithUser({ userId: 'u1', roleId: 'r1' })),
).rejects.toThrow(/SETTINGS\.delete/);
});
});