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') + }) + }) })