From 3c6b765e003a5d60384e07bec8f073c9c7130c31 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 9 Sep 2026 16:20:19 +0200 Subject: [PATCH] =?UTF-8?q?test(server-nestjs):=20parit=C3=A9=20reconcile-?= =?UTF-8?q?only=20des=20r=C3=B4les=20admin=20keycloak=20+=20garde=20d'?= =?UTF-8?q?=C3=A9mission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keycloak.service.spec.ts : verrouille la synchronisation des groupes OIDC admin via le cron reconcile uniquement et ajoute une garde négative (pas d'emitAdminRoleEvent sur AppEventsService) pour forcer tout futur chemin évènementiel à passer par l'audit de parité. - app-events.service.spec.ts : épingle la surface d'émission publique (emitProjectEvent/emitProjectMemberEvent/emitRepositoryEvent) et interdit l'apparition accidentelle d'un emitZoneEvent tant que la migration zone n'est pas passée par l'audit de parité. Refs #2574 #2576 Signed-off-by: William Phetsinorath Change-Id: Ia5161f97df87545876558fcb51f463bb6a6a6964 --- .../modules/events/app-events.service.spec.ts | 13 ++++++++ .../modules/keycloak/keycloak.service.spec.ts | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/apps/server-nestjs/src/modules/events/app-events.service.spec.ts b/apps/server-nestjs/src/modules/events/app-events.service.spec.ts index 35fa8759c4..f0949a9b4c 100644 --- a/apps/server-nestjs/src/modules/events/app-events.service.spec.ts +++ b/apps/server-nestjs/src/modules/events/app-events.service.spec.ts @@ -175,4 +175,17 @@ describe('appEventsService', () => { data: expect.objectContaining({ args: payload }), })) }) + + // Garde de parité : zone.* est écouté par Vault mais émis uniquement par le legacy. + describe('emit surface (migration parity)', () => { + const expectedEmitMethods = ['emitProjectEvent', 'emitProjectMemberEvent', 'emitRepositoryEvent'] as const + + it.each(expectedEmitMethods)('exposes %s as a public emit entrypoint', (method) => { + expect(typeof service[method]).toBe('function') + }) + + it('does NOT expose a zone emit entrypoint (zone.* listeners have no emitter yet — legacy owns the route)', () => { + expect('emitZoneEvent' in service).toBe(false) + }) + }) }) diff --git a/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts b/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts index b557a04ccd..3bd89eef95 100644 --- a/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts +++ b/apps/server-nestjs/src/modules/keycloak/keycloak.service.spec.ts @@ -1,5 +1,6 @@ import type { DeepMockProxy } from 'vitest-mock-extended' import type { AdminRoleWithDetails, ProjectWithDetails, UserWithAdminRoles } from './keycloak-datastore.service' +import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import { mockDeep } from 'vitest-mock-extended' @@ -526,4 +527,35 @@ describe('keycloakService', () => { expect(keycloak.removeUserFromGroup).toHaveBeenCalledWith('user-2', 'system-managed-id') }) }) + + describe('migration parity: admin-role event path (legacy hook.adminRole.*)', () => { + const adminRoleId = faker.string.uuid() + const adminRoles: AdminRoleWithDetails[] = [ + { id: adminRoleId, name: faker.string.alphanumeric(8), permissions: 0n, position: 0, oidcGroup: '/admin-group', type: 'managed' }, + ] + const users: UserWithAdminRoles[] = [ + { id: faker.string.uuid(), adminRoleIds: [adminRoleId] }, + ] + + it('syncs admin-role OIDC groups via cron reconcile ONLY (no event path exists)', async () => { + datastore.getAllProjects.mockResolvedValue([]) + datastore.getAllAdminRoles.mockResolvedValue(adminRoles) + datastore.getAllUsersWithAdminRoleIds.mockResolvedValue(users) + + const adminGroup = makeGroupRepresentation({ id: 'kc-parity-group-id', name: 'admin', path: '/console/parity-admin' }) + keycloak.getOrCreateGroupByPath.mockResolvedValue(adminGroup) + keycloak.getGroupMembers.mockResolvedValue([]) + + await service.handleCron() + + // Pas d'emit adminRole.* : la sync passe uniquement par le cron reconcile. + expect(keycloak.getOrCreateGroupByPath).toHaveBeenCalledWith('/admin-group') + }) + + it('exposes NO adminRole emit entrypoint on AppEventsService (cutover guard)', async () => { + const { AppEventsService } = await import('../events/app-events.service') + expect(Object.getOwnPropertyNames(AppEventsService.prototype)) + .not.toContain('emitAdminRoleEvent') + }) + }) })