From 41e0c2f80da3f55ca0cd250955d74256549824b9 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 8 Oct 2026 10:56:46 +0200 Subject: [PATCH 1/2] fix(server-nestjs): return the refreshed admin role list on create Legacy apps/server and the client store both expect POST /api/v1/admin/roles to answer the full role array; server-nestjs answered the created entity alone, crashing the roles screen on next .filter. Align the contract (201 + array) and the NestJS handler with the legacy behavior. Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ib088d8558aea8c0d73e6104a889a08ca6a6a6964 --- .../src/modules/admin-role/admin-role.controller.ts | 2 +- .../src/modules/admin-role/admin-role.service.spec.ts | 7 ++++--- .../src/modules/admin-role/admin-role.service.ts | 8 +++++--- packages/shared/src/contracts/admin-role.ts | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-role/admin-role.controller.ts b/apps/server-nestjs/src/modules/admin-role/admin-role.controller.ts index e0e09f05eb..379b9acb21 100644 --- a/apps/server-nestjs/src/modules/admin-role/admin-role.controller.ts +++ b/apps/server-nestjs/src/modules/admin-role/admin-role.controller.ts @@ -28,7 +28,7 @@ export class AdminRoleController { @RequireAdminPermission('ManageRoles') async createAdminRole( @Body(new ZodValidationPipe(CreateAdminRoleBodySchema)) body: CreateAdminRoleBody, - ): Promise { + ): Promise { return this.adminRoleService.create(body) } diff --git a/apps/server-nestjs/src/modules/admin-role/admin-role.service.spec.ts b/apps/server-nestjs/src/modules/admin-role/admin-role.service.spec.ts index b2ee4dcd24..048b14b4e7 100644 --- a/apps/server-nestjs/src/modules/admin-role/admin-role.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-role/admin-role.service.spec.ts @@ -22,7 +22,7 @@ describe('adminRoleService', () => { eventEmitter.emitAsync.mockResolvedValue([]) }) - it('creates a role at the next position and returns the created role', async () => { + it('creates a role at the next position and returns the refreshed role list', async () => { const existingRole = makeAdminRole({ position: 5, permissions: 4n, @@ -39,6 +39,7 @@ describe('adminRoleService', () => { prisma.adminRole.findFirst.mockResolvedValue(existingRole) prisma.adminRole.create.mockResolvedValue(createdRole) prisma.adminRole.findUnique.mockResolvedValue(createdRole) + prisma.adminRole.findMany.mockResolvedValue([createdRole]) prisma.user.findMany.mockResolvedValue([]) prisma.$transaction.mockImplementation(async callback => callback(prisma)) @@ -46,13 +47,13 @@ describe('adminRoleService', () => { const service = new AdminRoleService(prisma, eventEmitter) const result = await service.create(createBody) - expect(result).toEqual( + expect(result).toEqual([ expect.objectContaining({ id: existingRole.id, permissions: '0', position: 6, }), - ) + ]) expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'New role', diff --git a/apps/server-nestjs/src/modules/admin-role/admin-role.service.ts b/apps/server-nestjs/src/modules/admin-role/admin-role.service.ts index eb2d553be6..58eb458d44 100644 --- a/apps/server-nestjs/src/modules/admin-role/admin-role.service.ts +++ b/apps/server-nestjs/src/modules/admin-role/admin-role.service.ts @@ -12,7 +12,7 @@ import { getAdminRoleMemberCounts, getRoles, } from './admin-role-queries.utils' -import { toAdminRole, toAdminRoles } from './admin-role.utils' +import { toAdminRoles } from './admin-role.utils' @Injectable() export class AdminRoleService { @@ -34,7 +34,7 @@ export class AdminRoleService { } @StartActiveSpan() - async create(body: CreateAdminRoleBody): Promise { + async create(body: CreateAdminRoleBody): Promise { const span = trace.getActiveSpan() this.logger.log(`adminRole.create started (name=${body.name})`) @@ -54,7 +54,9 @@ export class AdminRoleService { span?.setAttribute('admin_role.id', createdRole.id) this.logger.log(`adminRole.create completed (id=${createdRole.id})`) - return toAdminRole(createdRole) + const roles = await getRoles(this.prisma) + span?.setAttribute('admin_role.count', roles.length) + return toAdminRoles(roles) } @StartActiveSpan() diff --git a/packages/shared/src/contracts/admin-role.ts b/packages/shared/src/contracts/admin-role.ts index e8030f532a..5e75bd5f51 100644 --- a/packages/shared/src/contracts/admin-role.ts +++ b/packages/shared/src/contracts/admin-role.ts @@ -23,7 +23,7 @@ export const adminRoleContract = contractInstance.router({ path: '', body: AdminRoleSchema.pick({ name: true }), responses: { - 200: AdminRoleSchema, + 201: AdminRoleSchema.array(), 400: ErrorSchema, 401: ErrorSchema, 403: ErrorSchema, From bd69cbdfdad823a6eb9da6964ef6090a0ba43d8c Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 8 Oct 2026 11:18:11 +0200 Subject: [PATCH 2/2] test(server): align createAdminRole spec fixture with list contract Signed-off-by: William Phetsinorath Change-Id: Ic50fb8d7e128232ac65cf051ada30a7e6a6a6964 --- apps/server/src/resources/admin-role/router.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/server/src/resources/admin-role/router.spec.ts b/apps/server/src/resources/admin-role/router.spec.ts index dab0f79268..960229f38e 100644 --- a/apps/server/src/resources/admin-role/router.spec.ts +++ b/apps/server/src/resources/admin-role/router.spec.ts @@ -41,11 +41,11 @@ describe('test adminRoleContract', () => { describe('createAdminRole', () => { it('should create a role for authorized users', async () => { const user = getUserMockInfos(ADMIN_PERMS.MANAGE_ROLES) - const newRole = { id: 'newRole', name: 'New Role' } + const newRoles = [{ id: faker.string.uuid(), name: 'New Role', oidcGroup: '', position: 0, permissions: '0', type: 'managed' }] const roleData = { name: 'New Role' } authUserMock.mockResolvedValueOnce(user) - businessCreateRoleMock.mockResolvedValueOnce(newRole) + businessCreateRoleMock.mockResolvedValueOnce(newRoles) const response = await app.inject() .post(adminRoleContract.createAdminRole.path) @@ -53,7 +53,7 @@ describe('test adminRoleContract', () => { .end() expect(businessCreateRoleMock).toHaveBeenCalledWith(roleData, expect.any(String)) - expect(response.json()).toEqual(newRole) + expect(response.json()).toEqual(newRoles) expect(response.statusCode).toEqual(201) })