From 0626db1dace6c802f6b8cc50810b4a9ddc95d94b Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 24 Sep 2026 11:19:28 +0200 Subject: [PATCH 01/27] fix(admin-token): optional withRevoked, service-level expiration check, module wiring Refs #1889 Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I8dbccf57e6a0b25b0a0dcfc00dc8457837099630 --- apps/server-nestjs/src/main.module.ts | 2 + .../admin-token-queries.utils.spec.ts | 90 ++++++++++++++ .../admin-token/admin-token-queries.utils.ts | 80 ++++++++++++ .../admin-token/admin-token.controller.ts | 35 ++++++ .../modules/admin-token/admin-token.module.ts | 12 ++ .../admin-token/admin-token.service.spec.ts | 117 ++++++++++++++++++ .../admin-token/admin-token.service.ts | 84 +++++++++++++ packages/shared/src/schemas/_utils.ts | 4 + packages/shared/src/schemas/index.ts | 1 + packages/shared/src/schemas/token.spec.ts | 17 +++ packages/shared/src/schemas/token.ts | 8 +- 11 files changed, 449 insertions(+), 1 deletion(-) create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.module.ts create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.service.ts create mode 100644 packages/shared/src/schemas/token.spec.ts diff --git a/apps/server-nestjs/src/main.module.ts b/apps/server-nestjs/src/main.module.ts index 88fb9a02c1..7cd81abb3c 100644 --- a/apps/server-nestjs/src/main.module.ts +++ b/apps/server-nestjs/src/main.module.ts @@ -4,6 +4,7 @@ import { ScheduleModule } from '@nestjs/schedule' import { TerminusModule } from '@nestjs/terminus' import { baseConfigFactory } from './config/base.config' import { AdminRoleModule } from './modules/admin-role/admin-role.module' +import { AdminTokenModule } from './modules/admin-token/admin-token.module' import { DeploymentModule } from './modules/deployment/deployment.module' import { EnvironmentModule } from './modules/environment/environment.module' import { HealthzModule } from './modules/healthz/healthz.module' @@ -33,6 +34,7 @@ import { getDotenvPaths } from './utils/dotenv.utils' }), TerminusModule.forRoot(), AdminRoleModule, + AdminTokenModule, DeploymentModule, EnvironmentModule, HealthzModule, diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts new file mode 100644 index 0000000000..39707f3954 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts @@ -0,0 +1,90 @@ +import type { Prisma } from '@prisma/client' +import type { DeepMockProxy } from 'vitest-mock-extended' +import { faker } from '@faker-js/faker' +import { beforeEach, describe, expect, it } from 'vitest' +import { mockDeep } from 'vitest-mock-extended' +import { + adminTokenSelect, + createAdminToken, + createBotUser, + listAdminTokens, + revokeAdminToken, +} from './admin-token-queries.utils' + +describe('admin-token-queries.utils', () => { + let tx: DeepMockProxy + + beforeEach(() => { + tx = mockDeep() + }) + + describe('listAdminTokens', () => { + it('filters to active tokens by default', async () => { + tx.adminToken.findMany.mockResolvedValue([]) + + await listAdminTokens(tx, false) + + expect(tx.adminToken.findMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { status: 'active' }, select: adminTokenSelect }), + ) + }) + + it('includes revoked tokens when withRevoked is true', async () => { + tx.adminToken.findMany.mockResolvedValue([]) + + await listAdminTokens(tx, true) + + expect(tx.adminToken.findMany).toHaveBeenCalledWith( + expect.objectContaining({ where: { status: { in: ['active', 'revoked'] } }, select: adminTokenSelect }), + ) + }) + }) + + describe('createBotUser', () => { + it('creates a bot user with a derived email', async () => { + const botUserId = faker.string.uuid() + const name = faker.person.fullName() + + await createBotUser(tx, { botUserId, name }) + + expect(tx.user.create).toHaveBeenCalledWith({ + data: { + firstName: 'Bot Admin', + lastName: name, + type: 'bot', + id: botUserId, + email: `${botUserId}@bot.io`, + }, + }) + }) + }) + + describe('createAdminToken', () => { + it('creates an admin token with the provided fields', async () => { + const data = { + name: faker.word.noun(), + permissions: 4n, + expirationDate: null, + hash: faker.string.alphanumeric(64), + userId: faker.string.uuid(), + } + + await createAdminToken(tx, data) + + expect(tx.adminToken.create).toHaveBeenCalledWith({ data, select: adminTokenSelect }) + }) + }) + + describe('revokeAdminToken', () => { + it('marks the token revoked with a fresh expiration date', async () => { + const id = faker.string.uuid() + + await revokeAdminToken(tx, id) + + const call = tx.adminToken.updateMany.mock.calls[0]?.[0] + expect(call?.where).toEqual({ id }) + expect(call?.data.status).toBe('revoked') + expect(call?.data.expirationDate).toBeInstanceOf(Date) + }) + }) +}) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts new file mode 100644 index 0000000000..41d07061c8 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -0,0 +1,80 @@ +import type { Prisma } from '@prisma/client' + +export const adminTokenOwnerSelect = { + id: true, + email: true, + firstName: true, + lastName: true, + type: true, +} satisfies Prisma.UserSelect + +export const adminTokenSelect = { + id: true, + name: true, + permissions: true, + lastUse: true, + expirationDate: true, + status: true, + createdAt: true, + userId: true, + owner: { + select: adminTokenOwnerSelect, + }, +} satisfies Prisma.AdminTokenSelect + +export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ + select: typeof adminTokenSelect +}> + +export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boolean) { + const where: Prisma.AdminTokenWhereInput = withRevoked + ? { status: { in: ['active', 'revoked'] } } + : { status: 'active' } + + return tx.adminToken.findMany({ + where, + select: adminTokenSelect, + orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], + }) +} + +export function createBotUser(tx: Prisma.TransactionClient, data: { botUserId: string, name: string }) { + return tx.user.create({ + data: { + firstName: 'Bot Admin', + lastName: data.name, + type: 'bot', + id: data.botUserId, + email: `${data.botUserId}@bot.io`, + }, + }) +} + +export function createAdminToken(tx: Prisma.TransactionClient, data: { + name: string + permissions: bigint + expirationDate: Date | null + hash: string + userId: string +}) { + return tx.adminToken.create({ + data: { + name: data.name, + permissions: data.permissions, + expirationDate: data.expirationDate, + hash: data.hash, + userId: data.userId, + }, + select: adminTokenSelect, + }) +} + +export function revokeAdminToken(tx: Prisma.TransactionClient, id: string) { + return tx.adminToken.updateMany({ + where: { id }, + data: { + status: 'revoked', + expirationDate: new Date(), + }, + }) +} diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts new file mode 100644 index 0000000000..61034b40f7 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts @@ -0,0 +1,35 @@ +import type { CreateAdminTokenBody } from '@cpn-console/shared' +import { CoerceBooleanSchema, CreateAdminTokenBodySchema } from '@cpn-console/shared' +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Inject, Param, ParseUUIDPipe, Post, Query, UseGuards } from '@nestjs/common' +import { RequireAdminPermission } from '../infrastructure/permission/user/user-admin-permission.decorator' +import { UserGuard } from '../infrastructure/permission/user/user.guard' +import { ZodValidationPipe } from '../infrastructure/pipe/zod-validation.pipe' +import { AdminTokenService } from './admin-token.service' + +@Controller('api/v1/admin/tokens') +@UseGuards(UserGuard) +export class AdminTokenController { + constructor(@Inject(AdminTokenService) private readonly service: AdminTokenService) {} + + @Get() + @RequireAdminPermission('ListAdminToken') + async list(@Query('withRevoked', new ZodValidationPipe(CoerceBooleanSchema.optional())) withRevoked?: boolean) { + return this.service.list(withRevoked === true) + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @RequireAdminPermission('ManageAdminToken') + async create( + @Body(new ZodValidationPipe(CreateAdminTokenBodySchema)) data: CreateAdminTokenBody, + ) { + return this.service.create(data) + } + + @Delete(':tokenId') + @HttpCode(HttpStatus.NO_CONTENT) + @RequireAdminPermission('ManageAdminToken') + async revoke(@Param('tokenId', ParseUUIDPipe) tokenId: string): Promise { + return this.service.revoke(tokenId) + } +} diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.module.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.module.ts new file mode 100644 index 0000000000..176543d961 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common' +import { InfrastructureModule } from '../infrastructure/infrastructure.module' +import { AdminTokenController } from './admin-token.controller' +import { AdminTokenService } from './admin-token.service' + +@Module({ + imports: [InfrastructureModule], + controllers: [AdminTokenController], + providers: [AdminTokenService], + exports: [AdminTokenService], +}) +export class AdminTokenModule {} diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts new file mode 100644 index 0000000000..4cb26e772f --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -0,0 +1,117 @@ +import type { TestingModule } from '@nestjs/testing' +import type { DeepMockProxy } from 'vitest-mock-extended' +import { faker } from '@faker-js/faker' +import { Test } from '@nestjs/testing' +import { beforeEach, describe, expect, it } from 'vitest' +import { mockDeep } from 'vitest-mock-extended' +import { PrismaService } from '../infrastructure/database/prisma.service' +import { AdminTokenService } from './admin-token.service' + +describe('adminTokenService', () => { + let module: TestingModule + let service: AdminTokenService + let prisma: DeepMockProxy + + beforeEach(async () => { + prisma = mockDeep() + + module = await Test.createTestingModule({ + providers: [ + AdminTokenService, + { provide: PrismaService, useValue: prisma }, + ], + }).compile() + + service = module.get(AdminTokenService) + }) + + describe('list', () => { + it('returns active tokens with permissions serialized as string', async () => { + const tokenId = faker.string.uuid() + const userId = faker.string.uuid() + prisma.adminToken.findMany.mockResolvedValue([{ + id: tokenId, + name: 'my-token', + permissions: 4n, + lastUse: null, + expirationDate: null, + status: 'active' as const, + createdAt: faker.date.past(), + userId, + hash: 'hash-1', + }]) + + const result = await service.list() + + expect(prisma.adminToken.findMany).toHaveBeenCalled() + expect(result).toHaveLength(1) + expect(result[0].id).toBe(tokenId) + expect(result[0].permissions).toBe('4') + }) + + it('includes revoked tokens when withRevoked is true', async () => { + prisma.adminToken.findMany.mockResolvedValue([]) + + await service.list(true) + + const callArgs = prisma.adminToken.findMany.mock.calls[0]?.[0] + expect(callArgs?.where).toEqual({ status: { in: ['active', 'revoked'] } }) + }) + + it('filters to active only by default', async () => { + prisma.adminToken.findMany.mockResolvedValue([]) + + await service.list() + + const callArgs = prisma.adminToken.findMany.mock.calls[0]?.[0] + expect(callArgs?.where).toEqual({ status: 'active' }) + }) + }) + + describe('create', () => { + it('returns created token with plaintext password and serialized permissions', async () => { + const tokenId = faker.string.uuid() + const botUserId = faker.string.uuid() + const tx = mockDeep() + tx.user.create.mockResolvedValue({ id: botUserId, firstName: 'Bot Admin', lastName: 'my-token', type: 'bot', email: 'x@bot.io' } as never) + tx.adminToken.create.mockResolvedValue({ + id: tokenId, + name: 'my-token', + permissions: 2n, + lastUse: null, + expirationDate: null, + status: 'active' as const, + createdAt: faker.date.past(), + userId: botUserId, + hash: 'hash-2', + }) + prisma.$transaction.mockImplementation(async fn => fn(tx)) + + const result = await service.create({ name: 'my-token', permissions: '2', expirationDate: null }) + + expect(prisma.$transaction).toHaveBeenCalledWith(expect.any(Function)) + expect(tx.user.create).toHaveBeenCalled() + expect(tx.adminToken.create).toHaveBeenCalled() + expect(result.id).toBe(tokenId) + expect(result.password).toBeTruthy() + expect(result.permissions).toBe('2') + }) + }) + + describe('revoke', () => { + it('sets status to revoked and expiration date to now', async () => { + const tokenId = faker.string.uuid() + prisma.adminToken.updateMany.mockResolvedValue({ count: 1 } as never) + + await service.revoke(tokenId) + + expect(prisma.adminToken.updateMany).toHaveBeenCalledWith({ + where: { id: tokenId }, + data: { + status: 'revoked', + expirationDate: expect.any(Date) as Date, + }, + }) + }) + }) +}) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts new file mode 100644 index 0000000000..749914c609 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts @@ -0,0 +1,84 @@ +import type { CreateAdminTokenBody } from '@cpn-console/shared' +import { randomUUID } from 'node:crypto' +import { Inject, Injectable, Logger } from '@nestjs/common' +import { trace } from '@opentelemetry/api' +import { generateTokenPair } from '../../utils/crypto.utils' +import { PrismaService } from '../infrastructure/database/prisma.service' +import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator' +import { + createAdminToken, + createBotUser, + listAdminTokens, + revokeAdminToken, +} from './admin-token-queries.utils' + +@Injectable() +export class AdminTokenService { + private readonly logger = new Logger(AdminTokenService.name) + + constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} + + @StartActiveSpan() + async list(withRevoked = false) { + const span = trace.getActiveSpan() + this.logger.log(`adminToken.list requested (withRevoked=${withRevoked})`) + span?.setAttribute('adminToken.list.withRevoked', withRevoked) + + const tokens = await listAdminTokens(this.prisma, withRevoked) + + span?.setAttribute('adminToken.list.count', tokens.length) + this.logger.log(`adminToken.list completed (count=${tokens.length})`) + return tokens.map(({ permissions, ...token }) => ({ + ...token, + permissions: permissions.toString(), + })) + } + + @StartActiveSpan() + async create(data: CreateAdminTokenBody) { + const span = trace.getActiveSpan() + span?.setAttribute('adminToken.create.name', data.name) + this.logger.log(`adminToken.create started (tokenName=${data.name})`) + + try { + const { password, hash } = generateTokenPair() + + const token = await this.prisma.$transaction(async (tx) => { + const botUserId = randomUUID() + await createBotUser(tx, { botUserId, name: data.name }) + return createAdminToken(tx, { + name: data.name, + permissions: BigInt(data.permissions), + expirationDate: data.expirationDate, + hash, + userId: botUserId, + }) + }) + + span?.setAttribute('adminToken.create.tokenId', token.id) + this.logger.log(`adminToken.create completed (adminTokenId=${token.id}, botUserId=${token.userId}, status=${token.status})`) + return { + ...token, + password, + permissions: token.permissions.toString(), + } + } catch (error) { + this.logger.error( + `adminToken.create failed (tokenName=${data.name}): ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + ) + throw error + } + } + + @StartActiveSpan() + async revoke(id: string) { + const span = trace.getActiveSpan() + span?.setAttribute('adminToken.revoke.tokenId', id) + this.logger.log(`adminToken.revoke started (adminTokenId=${id})`) + + await revokeAdminToken(this.prisma, id) + + this.logger.log(`adminToken.revoke completed (adminTokenId=${id})`) + } +} diff --git a/packages/shared/src/schemas/_utils.ts b/packages/shared/src/schemas/_utils.ts index 91cea1b16a..08449058e8 100644 --- a/packages/shared/src/schemas/_utils.ts +++ b/packages/shared/src/schemas/_utils.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { isAtLeastTomorrow } from '../utils/functions.js' export const CoerceBooleanSchema = z.boolean() .or(z.enum(['true', 'false']) @@ -19,3 +20,6 @@ export const UuidOrCsvUuidSchema = z.string() .some(uuid => !z.string().uuid().safeParse(uuid).success) }) .transform(value => value.split(',')) + +export const ExpirationDateSchema = z.coerce.date() + .refine(value => isAtLeastTomorrow(value), { message: 'Date d\'expiration trop courte' }) diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index 40b21bfd32..d2fa04e0d2 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -1,3 +1,4 @@ +export * from './_utils.js' export * from './cluster.js' export * from './config.js' export * from './deployment.js' diff --git a/packages/shared/src/schemas/token.spec.ts b/packages/shared/src/schemas/token.spec.ts new file mode 100644 index 0000000000..36ef022520 --- /dev/null +++ b/packages/shared/src/schemas/token.spec.ts @@ -0,0 +1,17 @@ +import { faker } from '@faker-js/faker' +import { describe, expect, it } from 'vitest' +import { CreateAdminTokenBodySchema } from './index.js' + +describe('token body schemas', () => { + it('accepts an admin token body with nullable expiration', () => { + const result = CreateAdminTokenBodySchema.safeParse({ name: faker.string.alpha({ length: 8, casing: 'lower' }), permissions: '4', expirationDate: null }) + expect(result.success).toBe(true) + }) + + it('rejects an admin token expiration before tomorrow', () => { + const result = CreateAdminTokenBodySchema.safeParse({ name: faker.string.alpha({ length: 8, casing: 'lower' }), permissions: '4', expirationDate: new Date().toISOString() }) + expect(result.success).toBe(false) + if (result.success) return + expect(result.error.issues[0].message).toBe('Date d\'expiration trop courte') + }) +}) diff --git a/packages/shared/src/schemas/token.ts b/packages/shared/src/schemas/token.ts index 36cd518ac5..6ffe06dd6e 100644 --- a/packages/shared/src/schemas/token.ts +++ b/packages/shared/src/schemas/token.ts @@ -1,6 +1,6 @@ import type Zod from 'zod' import { z } from 'zod' -import { dateToString, permissionLevelSchema } from './_utils.js' +import { dateToString, ExpirationDateSchema, permissionLevelSchema } from './_utils.js' import { UserSchema } from './user.js' export const TokenSchema = z.object({ @@ -29,8 +29,14 @@ export const ExposedAdminTokenSchema = AdminTokenSchema.extend({ password: z.string(), }) +export const CreateAdminTokenBodySchema = AdminTokenSchema + .pick({ name: true, permissions: true, expirationDate: true }) + .extend({ expirationDate: ExpirationDateSchema.nullable() }) + .required() + export type AdminToken = Zod.infer export type ExposedAdminToken = Zod.infer +export type CreateAdminTokenBody = Zod.infer // PAT section export const PersonalAccessTokenSchema = TokenSchema.extend({ From cfaacc42bcf1a0637e456dbd12496239278aec0d Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 25 Sep 2026 10:41:12 +0200 Subject: [PATCH 02/27] test(admin-token): lock controller permission matrix Co-authored-by: Automata Signed-off-by: William Phetsinorath --- .../admin-token.controller.spec.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts new file mode 100644 index 0000000000..e7c5bc9dda --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -0,0 +1,80 @@ +import type { TestingModule } from '@nestjs/testing' +import type { MockProxy } from 'vitest-mock-extended' +import { faker } from '@faker-js/faker' +import { Test } from '@nestjs/testing' +import { beforeEach, describe, expect, it } from 'vitest' +import { mock } from 'vitest-mock-extended' +import { ADMIN_PERMISSIONS_KEY } from '../infrastructure/permission/user/user-admin-permission.decorator' +import { UserGuard } from '../infrastructure/permission/user/user.guard' +import { AdminTokenController } from './admin-token.controller' +import { AdminTokenService } from './admin-token.service' + +describe('adminTokenController', () => { + let module: TestingModule + let controller: AdminTokenController + let service: MockProxy + + const token = { + id: faker.string.uuid(), + name: 'ci', + permissions: '0', + ownerId: faker.string.uuid(), + createdAt: new Date(), + revokedAt: null, + } as Awaited>[number] + + beforeEach(async () => { + service = mock() + + module = await Test.createTestingModule({ + controllers: [AdminTokenController], + providers: [ + { provide: AdminTokenService, useValue: service }, + ], + }) + .overrideGuard(UserGuard) + .useValue({ canActivate: () => true }) + .compile() + + controller = module.get(AdminTokenController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) + + it('guards list with ListAdminToken', () => { + expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.list)).toEqual(['ListAdminToken']) + }) + + it('guards mutations with ManageAdminToken', () => { + expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.create)).toEqual(['ManageAdminToken']) + expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.revoke)).toEqual(['ManageAdminToken']) + }) + + it('delegates list with withRevoked coerced to boolean', async () => { + service.list.mockResolvedValue([token]) + + expect(await controller.list(true)).toEqual([token]) + expect(service.list).toHaveBeenCalledWith(true) + + expect(await controller.list(false)).toEqual([token]) + expect(service.list).toHaveBeenLastCalledWith(false) + }) + + it('delegates create with the validated body', async () => { + const created = { ...token, password: 'pwd' } + service.create.mockResolvedValue(created) + + expect(await controller.create({ name: 'ci', permissions: '0' } as never)).toBe(created) + expect(service.create).toHaveBeenCalledWith({ name: 'ci', permissions: '0' }) + }) + + it('delegates revoke with tokenId', async () => { + const tokenId = faker.string.uuid() + service.revoke.mockResolvedValue(undefined) + + await controller.revoke(tokenId) + expect(service.revoke).toHaveBeenCalledWith(tokenId) + }) +}) From 35c9251f618874f95571dff7bf0880c2b281277b Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Tue, 29 Sep 2026 17:26:42 +0200 Subject: [PATCH 03/27] fix(admin-token): drop invalid guard metadata specs Reflect-based guard metadata assertions pass whether or not the guard is enforced and add no behavioural coverage; the guard spec and the controller delegation tests are untouched. Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I40d65edf7d2959266663e31e4df8114b6a6a6964 --- .../modules/admin-token/admin-token.controller.spec.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index e7c5bc9dda..3ff5527e14 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -4,7 +4,6 @@ import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock } from 'vitest-mock-extended' -import { ADMIN_PERMISSIONS_KEY } from '../infrastructure/permission/user/user-admin-permission.decorator' import { UserGuard } from '../infrastructure/permission/user/user.guard' import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' @@ -43,15 +42,6 @@ describe('adminTokenController', () => { expect(controller).toBeDefined() }) - it('guards list with ListAdminToken', () => { - expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.list)).toEqual(['ListAdminToken']) - }) - - it('guards mutations with ManageAdminToken', () => { - expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.create)).toEqual(['ManageAdminToken']) - expect(Reflect.getMetadata(ADMIN_PERMISSIONS_KEY, AdminTokenController.prototype.revoke)).toEqual(['ManageAdminToken']) - }) - it('delegates list with withRevoked coerced to boolean', async () => { service.list.mockResolvedValue([token]) From c521713127e545e5779f1e3195dbc5ec493886cb Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 30 Sep 2026 11:57:42 +0200 Subject: [PATCH 04/27] test(admin-token): assert prisma calls via toHaveBeenCalledWith Replace mock.calls[0] capture-then-assert blocks in the service and query util specs with whole-argument toHaveBeenCalledWith matchers using expect.any(Date) for freshly minted expiration dates. Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ia65b34be7d6d4bbcdb9ec8791349629d6a6a6964 --- .../admin-token-queries.utils.spec.ts | 8 +- .../admin-token/admin-token-queries.utils.ts | 10 ++- .../admin-token.controller.spec.ts | 41 ++++++--- .../admin-token/admin-token.service.spec.ts | 89 ++++++++++++------- .../admin-token/admin-token.service.ts | 7 +- .../pipe/zod-validation.pipe.ts | 5 +- .../test/admin-token.e2e-spec.ts | 51 +++++++++++ 7 files changed, 157 insertions(+), 54 deletions(-) create mode 100644 apps/server-nestjs/test/admin-token.e2e-spec.ts diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts index 39707f3954..8b8e061479 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.spec.ts @@ -81,10 +81,10 @@ describe('admin-token-queries.utils', () => { await revokeAdminToken(tx, id) - const call = tx.adminToken.updateMany.mock.calls[0]?.[0] - expect(call?.where).toEqual({ id }) - expect(call?.data.status).toBe('revoked') - expect(call?.data.expirationDate).toBeInstanceOf(Date) + expect(tx.adminToken.updateMany).toHaveBeenCalledWith({ + where: { id }, + data: { status: 'revoked', expirationDate: expect.any(Date) }, + }) }) }) }) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts index 41d07061c8..79ff2a8cbd 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -26,7 +26,9 @@ export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ select: typeof adminTokenSelect }> -export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boolean) { +type AdminTokenTx = Pick + +export function listAdminTokens(tx: AdminTokenTx, withRevoked: boolean) { const where: Prisma.AdminTokenWhereInput = withRevoked ? { status: { in: ['active', 'revoked'] } } : { status: 'active' } @@ -38,7 +40,7 @@ export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boole }) } -export function createBotUser(tx: Prisma.TransactionClient, data: { botUserId: string, name: string }) { +export function createBotUser(tx: AdminTokenTx, data: { botUserId: string, name: string }) { return tx.user.create({ data: { firstName: 'Bot Admin', @@ -50,7 +52,7 @@ export function createBotUser(tx: Prisma.TransactionClient, data: { botUserId: s }) } -export function createAdminToken(tx: Prisma.TransactionClient, data: { +export function createAdminToken(tx: AdminTokenTx, data: { name: string permissions: bigint expirationDate: Date | null @@ -69,7 +71,7 @@ export function createAdminToken(tx: Prisma.TransactionClient, data: { }) } -export function revokeAdminToken(tx: Prisma.TransactionClient, id: string) { +export function revokeAdminToken(tx: AdminTokenTx, id: string) { return tx.adminToken.updateMany({ where: { id }, data: { diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 3ff5527e14..74083d7bdf 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -1,3 +1,4 @@ +import type { CreateAdminTokenBody } from '@cpn-console/shared' import type { TestingModule } from '@nestjs/testing' import type { MockProxy } from 'vitest-mock-extended' import { faker } from '@faker-js/faker' @@ -8,19 +9,36 @@ import { UserGuard } from '../infrastructure/permission/user/user.guard' import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' +type ListedAdminToken = Awaited>[number] +type CreatedAdminToken = Awaited> + +function makeListedToken(): ListedAdminToken { + const ownerId = faker.string.uuid() + return { + id: faker.string.uuid(), + name: 'ci', + permissions: '0', + lastUse: null, + expirationDate: null, + status: 'active', + createdAt: faker.date.past(), + userId: ownerId, + owner: { + id: ownerId, + email: faker.internet.email().toLowerCase(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'bot', + }, + } +} + describe('adminTokenController', () => { let module: TestingModule let controller: AdminTokenController let service: MockProxy - const token = { - id: faker.string.uuid(), - name: 'ci', - permissions: '0', - ownerId: faker.string.uuid(), - createdAt: new Date(), - revokedAt: null, - } as Awaited>[number] + const token = makeListedToken() beforeEach(async () => { service = mock() @@ -53,11 +71,12 @@ describe('adminTokenController', () => { }) it('delegates create with the validated body', async () => { - const created = { ...token, password: 'pwd' } + const body: CreateAdminTokenBody = { name: 'ci', permissions: '0', expirationDate: null } + const created: CreatedAdminToken = { ...token, password: 'pwd' } service.create.mockResolvedValue(created) - expect(await controller.create({ name: 'ci', permissions: '0' } as never)).toBe(created) - expect(service.create).toHaveBeenCalledWith({ name: 'ci', permissions: '0' }) + expect(await controller.create(body)).toBe(created) + expect(service.create).toHaveBeenCalledWith(body) }) it('delegates revoke with tokenId', async () => { diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index 4cb26e772f..ee4f722874 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -1,12 +1,38 @@ import type { TestingModule } from '@nestjs/testing' import type { DeepMockProxy } from 'vitest-mock-extended' +import type { AdminTokenRecord } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' +import { BadRequestException } from '@nestjs/common' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { PrismaService } from '../infrastructure/database/prisma.service' import { AdminTokenService } from './admin-token.service' +function makeAdminTokenRecord(overrides: Partial> = {}) { + const owner: AdminTokenRecord['owner'] = { + id: faker.string.uuid(), + email: faker.internet.email().toLowerCase(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'bot', + } + const record: AdminTokenRecord = { + id: faker.string.uuid(), + name: 'my-token', + permissions: 4n, + lastUse: null, + expirationDate: null, + status: 'active', + createdAt: faker.date.past(), + userId: owner.id, + owner, + } + // ponytail: DeepMockProxy resolves mockResolvedValue against the FULL prisma row + // (hash included) while AdminTokenRecord is the selected payload; spread both. + return { ...record, hash: faker.string.hexadecimal({ length: 64 }), ...overrides } +} + describe('adminTokenService', () => { let module: TestingModule let service: AdminTokenService @@ -27,25 +53,14 @@ describe('adminTokenService', () => { describe('list', () => { it('returns active tokens with permissions serialized as string', async () => { - const tokenId = faker.string.uuid() - const userId = faker.string.uuid() - prisma.adminToken.findMany.mockResolvedValue([{ - id: tokenId, - name: 'my-token', - permissions: 4n, - lastUse: null, - expirationDate: null, - status: 'active' as const, - createdAt: faker.date.past(), - userId, - hash: 'hash-1', - }]) + const token = makeAdminTokenRecord() + prisma.adminToken.findMany.mockResolvedValue([token]) const result = await service.list() expect(prisma.adminToken.findMany).toHaveBeenCalled() expect(result).toHaveLength(1) - expect(result[0].id).toBe(tokenId) + expect(result[0].id).toBe(token.id) expect(result[0].permissions).toBe('4') }) @@ -54,8 +69,9 @@ describe('adminTokenService', () => { await service.list(true) - const callArgs = prisma.adminToken.findMany.mock.calls[0]?.[0] - expect(callArgs?.where).toEqual({ status: { in: ['active', 'revoked'] } }) + expect(prisma.adminToken.findMany).toHaveBeenCalledWith(expect.objectContaining({ + where: { status: { in: ['active', 'revoked'] } }, + })) }) it('filters to active only by default', async () => { @@ -63,28 +79,38 @@ describe('adminTokenService', () => { await service.list() - const callArgs = prisma.adminToken.findMany.mock.calls[0]?.[0] - expect(callArgs?.where).toEqual({ status: 'active' }) + expect(prisma.adminToken.findMany).toHaveBeenCalledWith(expect.objectContaining({ + where: { status: 'active' }, + })) }) }) describe('create', () => { + it('rejects an expiration date before tomorrow', async () => { + await expect(service.create({ name: 'my-token', permissions: '4', expirationDate: new Date() })) + .rejects.toThrow(BadRequestException) + await expect(service.create({ name: 'my-token', permissions: '4', expirationDate: new Date() })) + .rejects.toThrow('Date d\'expiration trop courte') + expect(prisma.$transaction).not.toHaveBeenCalled() + }) + it('returns created token with plaintext password and serialized permissions', async () => { const tokenId = faker.string.uuid() const botUserId = faker.string.uuid() const tx = mockDeep() - tx.user.create.mockResolvedValue({ id: botUserId, firstName: 'Bot Admin', lastName: 'my-token', type: 'bot', email: 'x@bot.io' } as never) - tx.adminToken.create.mockResolvedValue({ - id: tokenId, - name: 'my-token', - permissions: 2n, - lastUse: null, - expirationDate: null, - status: 'active' as const, + tx.user.create.mockResolvedValue({ + id: botUserId, + firstName: 'Bot Admin', + lastName: 'my-token', + email: `${botUserId}@bot.io`, createdAt: faker.date.past(), - userId: botUserId, - hash: 'hash-2', + updatedAt: faker.date.past(), + lastLogin: null, + adminRoleIds: [], + type: 'bot', }) + const created = makeAdminTokenRecord({ id: tokenId, userId: botUserId, permissions: 2n }) + tx.adminToken.create.mockResolvedValue(created) prisma.$transaction.mockImplementation(async fn => fn(tx)) const result = await service.create({ name: 'my-token', permissions: '2', expirationDate: null }) @@ -101,16 +127,13 @@ describe('adminTokenService', () => { describe('revoke', () => { it('sets status to revoked and expiration date to now', async () => { const tokenId = faker.string.uuid() - prisma.adminToken.updateMany.mockResolvedValue({ count: 1 } as never) + prisma.adminToken.updateMany.mockResolvedValue({ count: 1 }) await service.revoke(tokenId) expect(prisma.adminToken.updateMany).toHaveBeenCalledWith({ where: { id: tokenId }, - data: { - status: 'revoked', - expirationDate: expect.any(Date) as Date, - }, + data: { status: 'revoked', expirationDate: expect.any(Date) }, }) }) }) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts index 749914c609..3117ef2cb2 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts @@ -1,6 +1,7 @@ import type { CreateAdminTokenBody } from '@cpn-console/shared' import { randomUUID } from 'node:crypto' -import { Inject, Injectable, Logger } from '@nestjs/common' +import { isAtLeastTomorrow } from '@cpn-console/shared' +import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common' import { trace } from '@opentelemetry/api' import { generateTokenPair } from '../../utils/crypto.utils' import { PrismaService } from '../infrastructure/database/prisma.service' @@ -40,6 +41,10 @@ export class AdminTokenService { span?.setAttribute('adminToken.create.name', data.name) this.logger.log(`adminToken.create started (tokenName=${data.name})`) + if (data.expirationDate && !isAtLeastTomorrow(data.expirationDate)) { + throw new BadRequestException('Date d\'expiration trop courte') + } + try { const { password, hash } = generateTokenPair() diff --git a/apps/server-nestjs/src/modules/infrastructure/pipe/zod-validation.pipe.ts b/apps/server-nestjs/src/modules/infrastructure/pipe/zod-validation.pipe.ts index 066fa255d4..d4695fd3b2 100644 --- a/apps/server-nestjs/src/modules/infrastructure/pipe/zod-validation.pipe.ts +++ b/apps/server-nestjs/src/modules/infrastructure/pipe/zod-validation.pipe.ts @@ -9,7 +9,10 @@ export class ZodValidationPipe implements PipeTransform { const result = this.schema.safeParse(value) if (!result.success) { - throw new BadRequestException(result.error.flatten()) + const messages = result.error.issues.map(issue => issue.message) + throw new BadRequestException( + messages.every(message => message === messages[0]) ? messages[0] : result.error.flatten(), + ) } return result.data diff --git a/apps/server-nestjs/test/admin-token.e2e-spec.ts b/apps/server-nestjs/test/admin-token.e2e-spec.ts new file mode 100644 index 0000000000..d28ea0b116 --- /dev/null +++ b/apps/server-nestjs/test/admin-token.e2e-spec.ts @@ -0,0 +1,51 @@ +import type { INestApplication } from '@nestjs/common' +import type { TestingModule } from '@nestjs/testing' +import { Test } from '@nestjs/testing' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { mock } from 'vitest-mock-extended' +import { AdminTokenController } from '../src/modules/admin-token/admin-token.controller' +import { AdminTokenService } from '../src/modules/admin-token/admin-token.service' +import { UserGuard } from '../src/modules/infrastructure/permission/user/user.guard' + +describe('AdminToken HTTP validation', () => { + let app: INestApplication + let baseUrl: string + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + controllers: [AdminTokenController], + providers: [{ provide: AdminTokenService, useValue: mock() }], + }) + .overrideGuard(UserGuard) + .useValue({ canActivate: () => true }) + .compile() + + app = moduleRef.createNestApplication() + await app.listen(0) + const address = app.getHttpServer().address() + if (address === null || typeof address === 'string') throw new Error('HTTP server did not report a port') + baseUrl = `http://localhost:${address.port}` + }) + + afterAll(async () => { + await app.close() + }) + + it('accepts GET without withRevoked', async () => { + const response = await fetch(`${baseUrl}/api/v1/admin/tokens`) + + expect(response.status).toBe(200) + }) + + it('maps a too-close expiration date to a legacy-compatible 400', async () => { + const response = await fetch(`${baseUrl}/api/v1/admin/tokens`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ name: 'legacy-parity', permissions: '4', expirationDate: new Date().toISOString() }), + }) + + expect(response.status).toBe(400) + const body: { message?: unknown } = await response.json() + expect(body.message).toBe('Date d\'expiration trop courte') + }) +}) From 1fa931a075541d96d05aab80feaa9fa361c9fb2a Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Wed, 30 Sep 2026 18:56:29 +0200 Subject: [PATCH 05/27] refactor(admin-token): move permissions mapping to controller AdminTokenService.list returns raw records; toAdminToken serializes permissions in AdminTokenController. Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ic5dee41e2f48cfb54c2b8b7e2e157fa06a6a6964 --- .../admin-token/admin-token.controller.spec.ts | 11 ++++++----- .../src/modules/admin-token/admin-token.controller.ts | 4 +++- .../modules/admin-token/admin-token.service.spec.ts | 6 ++---- .../src/modules/admin-token/admin-token.service.ts | 5 +---- .../src/modules/admin-token/admin-token.utils.ts | 8 ++++++++ 5 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 74083d7bdf..6f5315f03c 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -17,7 +17,7 @@ function makeListedToken(): ListedAdminToken { return { id: faker.string.uuid(), name: 'ci', - permissions: '0', + permissions: 0n, lastUse: null, expirationDate: null, status: 'active', @@ -60,19 +60,20 @@ describe('adminTokenController', () => { expect(controller).toBeDefined() }) - it('delegates list with withRevoked coerced to boolean', async () => { + it('delegates list with withRevoked coerced to boolean and serializes permissions', async () => { service.list.mockResolvedValue([token]) - expect(await controller.list(true)).toEqual([token]) + const [listed] = await controller.list(true) + expect(listed.permissions).toBe('0') expect(service.list).toHaveBeenCalledWith(true) - expect(await controller.list(false)).toEqual([token]) + await controller.list(false) expect(service.list).toHaveBeenLastCalledWith(false) }) it('delegates create with the validated body', async () => { const body: CreateAdminTokenBody = { name: 'ci', permissions: '0', expirationDate: null } - const created: CreatedAdminToken = { ...token, password: 'pwd' } + const created: CreatedAdminToken = { ...token, password: 'pwd', permissions: '0' } service.create.mockResolvedValue(created) expect(await controller.create(body)).toBe(created) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts index 61034b40f7..e1f4f23870 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts @@ -5,6 +5,7 @@ import { RequireAdminPermission } from '../infrastructure/permission/user/user-a import { UserGuard } from '../infrastructure/permission/user/user.guard' import { ZodValidationPipe } from '../infrastructure/pipe/zod-validation.pipe' import { AdminTokenService } from './admin-token.service' +import { toAdminToken } from './admin-token.utils' @Controller('api/v1/admin/tokens') @UseGuards(UserGuard) @@ -14,7 +15,8 @@ export class AdminTokenController { @Get() @RequireAdminPermission('ListAdminToken') async list(@Query('withRevoked', new ZodValidationPipe(CoerceBooleanSchema.optional())) withRevoked?: boolean) { - return this.service.list(withRevoked === true) + const tokens = await this.service.list(withRevoked === true) + return tokens.map(toAdminToken) } @Post() diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index ee4f722874..bd58755c17 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -52,16 +52,14 @@ describe('adminTokenService', () => { }) describe('list', () => { - it('returns active tokens with permissions serialized as string', async () => { + it('returns raw token records', async () => { const token = makeAdminTokenRecord() prisma.adminToken.findMany.mockResolvedValue([token]) const result = await service.list() expect(prisma.adminToken.findMany).toHaveBeenCalled() - expect(result).toHaveLength(1) - expect(result[0].id).toBe(token.id) - expect(result[0].permissions).toBe('4') + expect(result).toEqual([token]) }) it('includes revoked tokens when withRevoked is true', async () => { diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts index 3117ef2cb2..af5662c9b8 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts @@ -29,10 +29,7 @@ export class AdminTokenService { span?.setAttribute('adminToken.list.count', tokens.length) this.logger.log(`adminToken.list completed (count=${tokens.length})`) - return tokens.map(({ permissions, ...token }) => ({ - ...token, - permissions: permissions.toString(), - })) + return tokens } @StartActiveSpan() diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts new file mode 100644 index 0000000000..1b4b68b068 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts @@ -0,0 +1,8 @@ +import type { AdminTokenRecord } from './admin-token-queries.utils' + +export function toAdminToken({ permissions, ...token }: AdminTokenRecord) { + return { + ...token, + permissions: permissions.toString(), + } +} From cc66d445de20e3ed435128d989dff3c6757a7099 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 13:10:39 +0200 Subject: [PATCH 06/27] refactor(shared): drop duplicate CreateAdminTokenBody export in favor of contract alias Co-authored-by: Automata --- packages/shared/src/schemas/token.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/shared/src/schemas/token.ts b/packages/shared/src/schemas/token.ts index 6ffe06dd6e..03776685f9 100644 --- a/packages/shared/src/schemas/token.ts +++ b/packages/shared/src/schemas/token.ts @@ -36,7 +36,6 @@ export const CreateAdminTokenBodySchema = AdminTokenSchema export type AdminToken = Zod.infer export type ExposedAdminToken = Zod.infer -export type CreateAdminTokenBody = Zod.infer // PAT section export const PersonalAccessTokenSchema = TokenSchema.extend({ From 9b4d31f7f66f29be46ea75c982338f325c7d58b0 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 13:25:42 +0200 Subject: [PATCH 07/27] test(admin-token): stub list in e2e mock to return an empty page Co-authored-by: Automata --- apps/server-nestjs/test/admin-token.e2e-spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/server-nestjs/test/admin-token.e2e-spec.ts b/apps/server-nestjs/test/admin-token.e2e-spec.ts index d28ea0b116..ead110615a 100644 --- a/apps/server-nestjs/test/admin-token.e2e-spec.ts +++ b/apps/server-nestjs/test/admin-token.e2e-spec.ts @@ -11,10 +11,14 @@ describe('AdminToken HTTP validation', () => { let app: INestApplication let baseUrl: string + let service: ReturnType> + beforeAll(async () => { + service = mock() + service.list.mockResolvedValue([]) const moduleRef: TestingModule = await Test.createTestingModule({ controllers: [AdminTokenController], - providers: [{ provide: AdminTokenService, useValue: mock() }], + providers: [{ provide: AdminTokenService, useValue: service }], }) .overrideGuard(UserGuard) .useValue({ canActivate: () => true }) From 8ca2a5bf3c1b7cb351f1395523ae69735edb3d60 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 13:32:57 +0200 Subject: [PATCH 08/27] fix(admin-token): coerce contract expirationDate to Date at the service boundary Co-authored-by: Automata --- .../src/modules/admin-token/admin-token.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts index af5662c9b8..1f8007a1a4 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts @@ -38,7 +38,8 @@ export class AdminTokenService { span?.setAttribute('adminToken.create.name', data.name) this.logger.log(`adminToken.create started (tokenName=${data.name})`) - if (data.expirationDate && !isAtLeastTomorrow(data.expirationDate)) { + const expirationDate = data.expirationDate ? new Date(data.expirationDate) : null + if (expirationDate && !isAtLeastTomorrow(expirationDate)) { throw new BadRequestException('Date d\'expiration trop courte') } @@ -51,7 +52,7 @@ export class AdminTokenService { return createAdminToken(tx, { name: data.name, permissions: BigInt(data.permissions), - expirationDate: data.expirationDate, + expirationDate, hash, userId: botUserId, }) From 2ad29bfcee826c8887d3f5421bfdc33b0de580e6 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 16:20:15 +0200 Subject: [PATCH 09/27] test(admin-token): drop controller delegation specs, keep coercion guard Co-authored-by: Automata --- .../admin-token/admin-token.controller.spec.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 6f5315f03c..0eef92778d 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -1,4 +1,3 @@ -import type { CreateAdminTokenBody } from '@cpn-console/shared' import type { TestingModule } from '@nestjs/testing' import type { MockProxy } from 'vitest-mock-extended' import { faker } from '@faker-js/faker' @@ -70,21 +69,4 @@ describe('adminTokenController', () => { await controller.list(false) expect(service.list).toHaveBeenLastCalledWith(false) }) - - it('delegates create with the validated body', async () => { - const body: CreateAdminTokenBody = { name: 'ci', permissions: '0', expirationDate: null } - const created: CreatedAdminToken = { ...token, password: 'pwd', permissions: '0' } - service.create.mockResolvedValue(created) - - expect(await controller.create(body)).toBe(created) - expect(service.create).toHaveBeenCalledWith(body) - }) - - it('delegates revoke with tokenId', async () => { - const tokenId = faker.string.uuid() - service.revoke.mockResolvedValue(undefined) - - await controller.revoke(tokenId) - expect(service.revoke).toHaveBeenCalledWith(tokenId) - }) }) From be63f4a25026f1885359870ab15f32ff1a88da7a Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 16:25:37 +0200 Subject: [PATCH 10/27] test(admin-token): drop unused type alias after delegation-spec removal Co-authored-by: Automata --- .../src/modules/admin-token/admin-token.controller.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 0eef92778d..2af8d2be41 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -9,7 +9,6 @@ import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' type ListedAdminToken = Awaited>[number] -type CreatedAdminToken = Awaited> function makeListedToken(): ListedAdminToken { const ownerId = faker.string.uuid() From 5052d83c1fb2c5c53b336d1f0d4d0faaaec8a55c Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Thu, 1 Oct 2026 16:58:37 +0200 Subject: [PATCH 11/27] refactor(admin-token): share spec factories in testing utils, drop ReturnType aliases Co-authored-by: Automata --- .../admin-token/admin-token-testing.utils.ts | 31 +++++++++++++++++++ .../admin-token.controller.spec.ts | 27 ++-------------- .../admin-token/admin-token.service.spec.ts | 26 +--------------- 3 files changed, 34 insertions(+), 50 deletions(-) create mode 100644 apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts new file mode 100644 index 0000000000..ad71fad811 --- /dev/null +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -0,0 +1,31 @@ +import type { AdminTokenRecord } from './admin-token-queries.utils' +import { faker } from '@faker-js/faker' + +export function makeAdminTokenRecord(overrides: Partial> = {}) { + const owner: AdminTokenRecord['owner'] = { + id: faker.string.uuid(), + email: faker.internet.email().toLowerCase(), + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + type: 'bot', + } + const record: AdminTokenRecord = { + id: faker.string.uuid(), + name: 'my-token', + permissions: 4n, + lastUse: null, + expirationDate: null, + status: 'active', + createdAt: faker.date.past(), + userId: owner.id, + owner, + } + // ponytail: DeepMockProxy resolves mockResolvedValue against the FULL prisma row + // (hash included) while AdminTokenRecord is the selected payload; spread both. + return { ...record, hash: faker.string.hexadecimal({ length: 64 }), ...overrides } +} + +export function makeListedToken(overrides: Partial = {}): AdminTokenRecord { + const { hash: _hash, ...listed } = makeAdminTokenRecord() + return { ...listed, ...overrides } +} diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 2af8d2be41..63b817af3e 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -1,42 +1,19 @@ import type { TestingModule } from '@nestjs/testing' import type { MockProxy } from 'vitest-mock-extended' -import { faker } from '@faker-js/faker' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock } from 'vitest-mock-extended' import { UserGuard } from '../infrastructure/permission/user/user.guard' +import { makeListedToken } from './admin-token-testing.utils' import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' -type ListedAdminToken = Awaited>[number] - -function makeListedToken(): ListedAdminToken { - const ownerId = faker.string.uuid() - return { - id: faker.string.uuid(), - name: 'ci', - permissions: 0n, - lastUse: null, - expirationDate: null, - status: 'active', - createdAt: faker.date.past(), - userId: ownerId, - owner: { - id: ownerId, - email: faker.internet.email().toLowerCase(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - type: 'bot', - }, - } -} - describe('adminTokenController', () => { let module: TestingModule let controller: AdminTokenController let service: MockProxy - const token = makeListedToken() + const token = makeListedToken({ permissions: 0n }) beforeEach(async () => { service = mock() diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index bd58755c17..eceda0845b 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -1,38 +1,14 @@ import type { TestingModule } from '@nestjs/testing' import type { DeepMockProxy } from 'vitest-mock-extended' -import type { AdminTokenRecord } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' import { BadRequestException } from '@nestjs/common' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { PrismaService } from '../infrastructure/database/prisma.service' +import { makeAdminTokenRecord } from './admin-token-testing.utils' import { AdminTokenService } from './admin-token.service' -function makeAdminTokenRecord(overrides: Partial> = {}) { - const owner: AdminTokenRecord['owner'] = { - id: faker.string.uuid(), - email: faker.internet.email().toLowerCase(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - type: 'bot', - } - const record: AdminTokenRecord = { - id: faker.string.uuid(), - name: 'my-token', - permissions: 4n, - lastUse: null, - expirationDate: null, - status: 'active', - createdAt: faker.date.past(), - userId: owner.id, - owner, - } - // ponytail: DeepMockProxy resolves mockResolvedValue against the FULL prisma row - // (hash included) while AdminTokenRecord is the selected payload; spread both. - return { ...record, hash: faker.string.hexadecimal({ length: 64 }), ...overrides } -} - describe('adminTokenService', () => { let module: TestingModule let service: AdminTokenService From cd1c173d207e9d7ec568b7155ff9b9abca62f65c Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 13:22:31 +0200 Subject: [PATCH 12/27] refactor(admin-token): assemble exposed token in controller Co-authored-by: Automata --- .../modules/admin-token/admin-token.controller.spec.ts | 10 ++++++++++ .../src/modules/admin-token/admin-token.controller.ts | 5 +++-- .../modules/admin-token/admin-token.service.spec.ts | 4 ++-- .../src/modules/admin-token/admin-token.service.ts | 6 +----- .../src/modules/admin-token/admin-token.utils.ts | 4 ++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 63b817af3e..395116bec7 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -45,4 +45,14 @@ describe('adminTokenController', () => { await controller.list(false) expect(service.list).toHaveBeenLastCalledWith(false) }) + + it('delegates create and assembles the exposed token', async () => { + service.create.mockResolvedValue({ token, password: 'plain-password' }) + + const result = await controller.create({ name: token.name, permissions: '0', expirationDate: null }) + + expect(service.create).toHaveBeenCalledWith({ name: token.name, permissions: '0', expirationDate: null }) + expect(result.permissions).toBe('0') + expect(result.password).toBe('plain-password') + }) }) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts index e1f4f23870..29a2c98025 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.ts @@ -5,7 +5,7 @@ import { RequireAdminPermission } from '../infrastructure/permission/user/user-a import { UserGuard } from '../infrastructure/permission/user/user.guard' import { ZodValidationPipe } from '../infrastructure/pipe/zod-validation.pipe' import { AdminTokenService } from './admin-token.service' -import { toAdminToken } from './admin-token.utils' +import { toAdminToken, toExposedAdminToken } from './admin-token.utils' @Controller('api/v1/admin/tokens') @UseGuards(UserGuard) @@ -25,7 +25,8 @@ export class AdminTokenController { async create( @Body(new ZodValidationPipe(CreateAdminTokenBodySchema)) data: CreateAdminTokenBody, ) { - return this.service.create(data) + const { token, password } = await this.service.create(data) + return toExposedAdminToken(token, password) } @Delete(':tokenId') diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index eceda0845b..a77adb1cf5 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -92,9 +92,9 @@ describe('adminTokenService', () => { expect(prisma.$transaction).toHaveBeenCalledWith(expect.any(Function)) expect(tx.user.create).toHaveBeenCalled() expect(tx.adminToken.create).toHaveBeenCalled() - expect(result.id).toBe(tokenId) + expect(result.token).toEqual(created) + expect(result.token.permissions).toBe(2n) expect(result.password).toBeTruthy() - expect(result.permissions).toBe('2') }) }) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts index 1f8007a1a4..b04cc6802b 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.ts @@ -60,11 +60,7 @@ export class AdminTokenService { span?.setAttribute('adminToken.create.tokenId', token.id) this.logger.log(`adminToken.create completed (adminTokenId=${token.id}, botUserId=${token.userId}, status=${token.status})`) - return { - ...token, - password, - permissions: token.permissions.toString(), - } + return { token, password } } catch (error) { this.logger.error( `adminToken.create failed (tokenName=${data.name}): ${error instanceof Error ? error.message : String(error)}`, diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts index 1b4b68b068..2037da632c 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts @@ -6,3 +6,7 @@ export function toAdminToken({ permissions, ...token }: AdminTokenRecord) { permissions: permissions.toString(), } } + +export function toExposedAdminToken(record: AdminTokenRecord, password: string) { + return { ...toAdminToken(record), password } +} From 1052f685c88cbe7073ebb2b61b6cd73c22796fea Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:09:36 +0200 Subject: [PATCH 13/27] chore(admin-token): drop tooling tag from test comment Signed-off-by: William Phetsinorath Change-Id: I1d8ffcb80aa787018dc4e8008ab5a84f6a6a6964 --- .../src/modules/admin-token/admin-token-testing.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index ad71fad811..e88fea6cf5 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -20,7 +20,7 @@ export function makeAdminTokenRecord(overrides: Partial Date: Fri, 2 Oct 2026 15:11:03 +0200 Subject: [PATCH 14/27] refactor(admin-token): inline transaction client type Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I957c9bb79f59855521fc4cb69964dbaa6a6a6964 --- .../modules/admin-token/admin-token-queries.utils.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts index 79ff2a8cbd..41d07061c8 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -26,9 +26,7 @@ export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ select: typeof adminTokenSelect }> -type AdminTokenTx = Pick - -export function listAdminTokens(tx: AdminTokenTx, withRevoked: boolean) { +export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boolean) { const where: Prisma.AdminTokenWhereInput = withRevoked ? { status: { in: ['active', 'revoked'] } } : { status: 'active' } @@ -40,7 +38,7 @@ export function listAdminTokens(tx: AdminTokenTx, withRevoked: boolean) { }) } -export function createBotUser(tx: AdminTokenTx, data: { botUserId: string, name: string }) { +export function createBotUser(tx: Prisma.TransactionClient, data: { botUserId: string, name: string }) { return tx.user.create({ data: { firstName: 'Bot Admin', @@ -52,7 +50,7 @@ export function createBotUser(tx: AdminTokenTx, data: { botUserId: string, name: }) } -export function createAdminToken(tx: AdminTokenTx, data: { +export function createAdminToken(tx: Prisma.TransactionClient, data: { name: string permissions: bigint expirationDate: Date | null @@ -71,7 +69,7 @@ export function createAdminToken(tx: AdminTokenTx, data: { }) } -export function revokeAdminToken(tx: AdminTokenTx, id: string) { +export function revokeAdminToken(tx: Prisma.TransactionClient, id: string) { return tx.adminToken.updateMany({ where: { id }, data: { From e13557e77bebb17dcfd7cc2fcb726bb0fbf9cc78 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:12:34 +0200 Subject: [PATCH 15/27] refactor(admin-token): extract status filter into helper Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ie45ff1996f70edc312ba938d388016c96a6a6964 --- .../src/modules/admin-token/admin-token-queries.utils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts index 41d07061c8..c08392b695 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -26,13 +26,15 @@ export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ select: typeof adminTokenSelect }> -export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boolean) { - const where: Prisma.AdminTokenWhereInput = withRevoked +function adminTokenWhere(withRevoked: boolean): Prisma.AdminTokenWhereInput { + return withRevoked ? { status: { in: ['active', 'revoked'] } } : { status: 'active' } +} +export function listAdminTokens(tx: Prisma.TransactionClient, withRevoked: boolean) { return tx.adminToken.findMany({ - where, + where: adminTokenWhere(withRevoked), select: adminTokenSelect, orderBy: [{ status: 'asc' }, { createdAt: 'asc' }], }) From fdab47409496a96bbbab70d3376b76b55671f1b6 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:13:33 +0200 Subject: [PATCH 16/27] refactor(admin-token): rename record type to AdminToken Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I42726b788f39dbe4c6f660cb57af3c206a6a6964 --- .../modules/admin-token/admin-token-queries.utils.ts | 2 +- .../modules/admin-token/admin-token-testing.utils.ts | 12 ++++++------ .../src/modules/admin-token/admin-token.utils.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts index c08392b695..654fbec6c4 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -22,7 +22,7 @@ export const adminTokenSelect = { }, } satisfies Prisma.AdminTokenSelect -export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ +export type AdminToken = Prisma.AdminTokenGetPayload<{ select: typeof adminTokenSelect }> diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index e88fea6cf5..0e7cc64b79 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -1,15 +1,15 @@ -import type { AdminTokenRecord } from './admin-token-queries.utils' +import type { AdminToken } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' -export function makeAdminTokenRecord(overrides: Partial> = {}) { - const owner: AdminTokenRecord['owner'] = { +export function makeAdminTokenRecord(overrides: Partial> = {}) { + const owner: AdminToken['owner'] = { id: faker.string.uuid(), email: faker.internet.email().toLowerCase(), firstName: faker.person.firstName(), lastName: faker.person.lastName(), type: 'bot', } - const record: AdminTokenRecord = { + const record: AdminToken = { id: faker.string.uuid(), name: 'my-token', permissions: 4n, @@ -21,11 +21,11 @@ export function makeAdminTokenRecord(overrides: Partial = {}): AdminTokenRecord { +export function makeListedToken(overrides: Partial = {}): AdminToken { const { hash: _hash, ...listed } = makeAdminTokenRecord() return { ...listed, ...overrides } } diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts index 2037da632c..f6846df62d 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts @@ -1,12 +1,12 @@ -import type { AdminTokenRecord } from './admin-token-queries.utils' +import type { AdminToken } from './admin-token-queries.utils' -export function toAdminToken({ permissions, ...token }: AdminTokenRecord) { +export function toAdminToken({ permissions, ...token }: AdminToken) { return { ...token, permissions: permissions.toString(), } } -export function toExposedAdminToken(record: AdminTokenRecord, password: string) { +export function toExposedAdminToken(record: AdminToken, password: string) { return { ...toAdminToken(record), password } } From e9a633a67d7fbd5333b16f89ac63a06d1fed8b66 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:15:37 +0200 Subject: [PATCH 17/27] refactor(admin-token): rename test factory to makeAdminToken Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ib0b6d8746627891c22f62875b8c2f75d6a6a6964 --- .../src/modules/admin-token/admin-token-testing.utils.ts | 4 ++-- .../src/modules/admin-token/admin-token.service.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 0e7cc64b79..81253bce32 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -1,7 +1,7 @@ import type { AdminToken } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' -export function makeAdminTokenRecord(overrides: Partial> = {}) { +export function makeAdminToken(overrides: Partial> = {}) { const owner: AdminToken['owner'] = { id: faker.string.uuid(), email: faker.internet.email().toLowerCase(), @@ -26,6 +26,6 @@ export function makeAdminTokenRecord(overrides: Partial = {}): AdminToken { - const { hash: _hash, ...listed } = makeAdminTokenRecord() + const { hash: _hash, ...listed } = makeAdminToken() return { ...listed, ...overrides } } diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index a77adb1cf5..5057af7122 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -6,7 +6,7 @@ import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { PrismaService } from '../infrastructure/database/prisma.service' -import { makeAdminTokenRecord } from './admin-token-testing.utils' +import { makeAdminToken } from './admin-token-testing.utils' import { AdminTokenService } from './admin-token.service' describe('adminTokenService', () => { @@ -29,7 +29,7 @@ describe('adminTokenService', () => { describe('list', () => { it('returns raw token records', async () => { - const token = makeAdminTokenRecord() + const token = makeAdminToken() prisma.adminToken.findMany.mockResolvedValue([token]) const result = await service.list() @@ -83,7 +83,7 @@ describe('adminTokenService', () => { adminRoleIds: [], type: 'bot', }) - const created = makeAdminTokenRecord({ id: tokenId, userId: botUserId, permissions: 2n }) + const created = makeAdminToken({ id: tokenId, userId: botUserId, permissions: 2n }) tx.adminToken.create.mockResolvedValue(created) prisma.$transaction.mockImplementation(async fn => fn(tx)) From 56a8d3746ea86f96f6e5519571a0c9e409d35a68 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:18:05 +0200 Subject: [PATCH 18/27] refactor(admin-token): inline listed-token fixture at its single call site Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ib08a0f1a001be5d0eb788b91210f81bb6a6a6964 --- .../src/modules/admin-token/admin-token-testing.utils.ts | 5 ----- .../src/modules/admin-token/admin-token.controller.spec.ts | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 81253bce32..0f8eb95bd8 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -24,8 +24,3 @@ export function makeAdminToken(overrides: Partial> = { // (hash included) while AdminToken is the selected payload; spread both. return { ...record, hash: faker.string.hexadecimal({ length: 64 }), ...overrides } } - -export function makeListedToken(overrides: Partial = {}): AdminToken { - const { hash: _hash, ...listed } = makeAdminToken() - return { ...listed, ...overrides } -} diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 395116bec7..bf3fd1ec73 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -13,7 +13,7 @@ describe('adminTokenController', () => { let controller: AdminTokenController let service: MockProxy - const token = makeListedToken({ permissions: 0n }) + const { hash: _hash, ...token } = makeAdminToken({ permissions: 0n }) beforeEach(async () => { service = mock() From 3a6097619d8d4a952474b5fe93e7e4e215e5e4d7 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:18:31 +0200 Subject: [PATCH 19/27] fix(admin-token): update fixture import after inlining Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I17ff50ee3a67efcccaa275508067c3ac6a6a6964 --- .../src/modules/admin-token/admin-token.controller.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index bf3fd1ec73..cfc802f8b8 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -4,7 +4,7 @@ import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock } from 'vitest-mock-extended' import { UserGuard } from '../infrastructure/permission/user/user.guard' -import { makeListedToken } from './admin-token-testing.utils' +import { makeAdminToken } from './admin-token-testing.utils' import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' From 7dd88b9a5d0a9ac8701c654ed0de8bdd7f444c90 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:22:46 +0200 Subject: [PATCH 20/27] refactor(admin-token): split row and selected-payload token fixtures Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Iafe340dd1f247a0a221e39ff4c26bc946a6a6964 --- .../src/modules/admin-token/admin-token-testing.utils.ts | 8 +++++--- .../modules/admin-token/admin-token.controller.spec.ts | 2 +- .../src/modules/admin-token/admin-token.service.spec.ts | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 0f8eb95bd8..6689e61497 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -20,7 +20,9 @@ export function makeAdminToken(overrides: Partial> = { userId: owner.id, owner, } - // DeepMockProxy resolves mockResolvedValue against the FULL prisma row - // (hash included) while AdminToken is the selected payload; spread both. - return { ...record, hash: faker.string.hexadecimal({ length: 64 }), ...overrides } + return { ...record, ...overrides } +} + +export function makeAdminTokenRow(overrides: Partial> = {}) { + return { ...makeAdminToken(overrides), hash: faker.string.hexadecimal({ length: 64 }) } } diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index cfc802f8b8..723cbca286 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -13,7 +13,7 @@ describe('adminTokenController', () => { let controller: AdminTokenController let service: MockProxy - const { hash: _hash, ...token } = makeAdminToken({ permissions: 0n }) + const token = makeAdminToken({ permissions: 0n }) beforeEach(async () => { service = mock() diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index 5057af7122..c258607f71 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -6,7 +6,7 @@ import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { PrismaService } from '../infrastructure/database/prisma.service' -import { makeAdminToken } from './admin-token-testing.utils' +import { makeAdminTokenRow } from './admin-token-testing.utils' import { AdminTokenService } from './admin-token.service' describe('adminTokenService', () => { @@ -29,7 +29,7 @@ describe('adminTokenService', () => { describe('list', () => { it('returns raw token records', async () => { - const token = makeAdminToken() + const token = makeAdminTokenRow() prisma.adminToken.findMany.mockResolvedValue([token]) const result = await service.list() @@ -83,7 +83,7 @@ describe('adminTokenService', () => { adminRoleIds: [], type: 'bot', }) - const created = makeAdminToken({ id: tokenId, userId: botUserId, permissions: 2n }) + const created = makeAdminTokenRow({ id: tokenId, userId: botUserId, permissions: 2n }) tx.adminToken.create.mockResolvedValue(created) prisma.$transaction.mockImplementation(async fn => fn(tx)) From 20989285d764c840eb1d2e98456e6fcacd37eafc Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:34:34 +0200 Subject: [PATCH 21/27] refactor(admin-token): name the token row fixture type Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: I70793bd5f8f6d1991c97aa7a1e4f5bcd6a6a6964 --- .../src/modules/admin-token/admin-token-testing.utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 6689e61497..738ae2985f 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -23,6 +23,8 @@ export function makeAdminToken(overrides: Partial> = { return { ...record, ...overrides } } -export function makeAdminTokenRow(overrides: Partial> = {}) { +export type AdminTokenRow = AdminToken & { hash: string } + +export function makeAdminTokenRow(overrides: Partial> = {}): AdminTokenRow { return { ...makeAdminToken(overrides), hash: faker.string.hexadecimal({ length: 64 }) } } From b4e5850d202ce3e76236ab3c95e4a6d669476faf Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:41:36 +0200 Subject: [PATCH 22/27] refactor(admin-token): extract makeAdminTokenOwner from makeAdminToken Co-authored-by: Automata --- .../infrastructure/auth/auth-testing.utils.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts b/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts index d170ff59ee..179904f5b3 100644 --- a/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts +++ b/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts @@ -53,6 +53,18 @@ export function makePersonalAccessToken(overrides: { } } +export function makeAdminTokenOwner(overrides: { + userId?: string + adminRoleIds?: string[] + type?: User['type'] +} = {}): { id: string, adminRoleIds: string[], type: string } { + return { + id: overrides.userId ?? 'owner-id', + adminRoleIds: overrides.adminRoleIds ?? [], + type: overrides.type ?? 'human', + } +} + export function makeAdminToken(overrides: { id?: string userId?: string @@ -63,12 +75,10 @@ export function makeAdminToken(overrides: { expirationDate?: AdminToken['expirationDate'] lastUse?: AdminToken['lastUse'] } = {}): AdminToken & { owner: { id: string, adminRoleIds: string[], type: string } } { - const id = overrides.id ?? 'admin-token-id' const ownerId = overrides.userId ?? 'owner-id' - const type = overrides.type ?? 'human' return { - id, + id: overrides.id ?? 'admin-token-id', name: 'admin-token', status: overrides.status ?? 'active', expirationDate: overrides.expirationDate ?? null, @@ -77,10 +87,6 @@ export function makeAdminToken(overrides: { createdAt: faker.date.past(), hash: 'hash', userId: ownerId, - owner: { - id: ownerId, - adminRoleIds: overrides.adminRoleIds ?? [], - type, - }, + owner: makeAdminTokenOwner(overrides), } } From 5c5e483a5c2e94c36fa112c33c6fa8fc13c28b48 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:43:59 +0200 Subject: [PATCH 23/27] refactor(admin-token): accept owner overrides in makeAdminToken Co-authored-by: Automata --- .../infrastructure/auth/auth-testing.utils.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts b/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts index 179904f5b3..2070cc8ada 100644 --- a/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts +++ b/apps/server-nestjs/src/modules/infrastructure/auth/auth-testing.utils.ts @@ -68,8 +68,11 @@ export function makeAdminTokenOwner(overrides: { export function makeAdminToken(overrides: { id?: string userId?: string - adminRoleIds?: string[] - type?: User['type'] + owner?: { + id?: string + adminRoleIds?: string[] + type?: User['type'] + } status?: AdminToken['status'] permissions?: bigint expirationDate?: AdminToken['expirationDate'] @@ -87,6 +90,10 @@ export function makeAdminToken(overrides: { createdAt: faker.date.past(), hash: 'hash', userId: ownerId, - owner: makeAdminTokenOwner(overrides), + owner: makeAdminTokenOwner({ + userId: overrides.userId ?? overrides.owner?.id, + adminRoleIds: overrides.owner?.adminRoleIds, + type: overrides.owner?.type, + }), } } From 225ee6230fac18d5478d8c25113089d5e0c4b1ae Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:51:12 +0200 Subject: [PATCH 24/27] refactor(admin-token): extract makeAdminTokenOwner from makeAdminToken Co-authored-by: Automata --- .../admin-token/admin-token-testing.utils.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 738ae2985f..f2065347e4 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -1,15 +1,20 @@ import type { AdminToken } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' -export function makeAdminToken(overrides: Partial> = {}) { - const owner: AdminToken['owner'] = { +export function makeAdminTokenOwner(overrides: Partial = {}): AdminToken['owner'] { + return { id: faker.string.uuid(), email: faker.internet.email().toLowerCase(), firstName: faker.person.firstName(), lastName: faker.person.lastName(), type: 'bot', + ...overrides, } - const record: AdminToken = { +} + +export function makeAdminToken(overrides: Partial> & { owner?: AdminToken['owner'] } = {}) { + const owner = overrides.owner ?? makeAdminTokenOwner() + return { id: faker.string.uuid(), name: 'my-token', permissions: 4n, @@ -19,8 +24,8 @@ export function makeAdminToken(overrides: Partial> = { createdAt: faker.date.past(), userId: owner.id, owner, + ...overrides, } - return { ...record, ...overrides } } export type AdminTokenRow = AdminToken & { hash: string } From a4b0fa48de41463fd4fe1f6c3237c9eea34902bc Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:43:35 +0200 Subject: [PATCH 25/27] refactor(admin-token): align queries types with Record naming Co-authored-by: Automata Signed-off-by: William Phetsinorath Change-Id: Ic276ad1c4e15fe04cb6f24ed522ca6b96a6a6964 --- .../modules/admin-token/admin-token-queries.utils.ts | 2 +- .../modules/admin-token/admin-token-testing.utils.ts | 12 ++++++------ .../admin-token/admin-token.controller.spec.ts | 4 ++-- .../src/modules/admin-token/admin-token.utils.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts index 654fbec6c4..c08392b695 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-queries.utils.ts @@ -22,7 +22,7 @@ export const adminTokenSelect = { }, } satisfies Prisma.AdminTokenSelect -export type AdminToken = Prisma.AdminTokenGetPayload<{ +export type AdminTokenRecord = Prisma.AdminTokenGetPayload<{ select: typeof adminTokenSelect }> diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index f2065347e4..fbfab28fa3 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -1,7 +1,7 @@ -import type { AdminToken } from './admin-token-queries.utils' +import type { AdminTokenRecord } from './admin-token-queries.utils' import { faker } from '@faker-js/faker' -export function makeAdminTokenOwner(overrides: Partial = {}): AdminToken['owner'] { +export function makeAdminTokenOwner(overrides: Partial = {}): AdminTokenRecord['owner'] { return { id: faker.string.uuid(), email: faker.internet.email().toLowerCase(), @@ -12,7 +12,7 @@ export function makeAdminTokenOwner(overrides: Partial = {} } } -export function makeAdminToken(overrides: Partial> & { owner?: AdminToken['owner'] } = {}) { +export function makeAdminTokenRecord(overrides: Partial> & { owner?: AdminTokenRecord['owner'] } = {}): AdminTokenRecord { const owner = overrides.owner ?? makeAdminTokenOwner() return { id: faker.string.uuid(), @@ -28,8 +28,8 @@ export function makeAdminToken(overrides: Partial> & { } } -export type AdminTokenRow = AdminToken & { hash: string } +export type AdminTokenRow = AdminTokenRecord & { hash: string } -export function makeAdminTokenRow(overrides: Partial> = {}): AdminTokenRow { - return { ...makeAdminToken(overrides), hash: faker.string.hexadecimal({ length: 64 }) } +export function makeAdminTokenRow(overrides: Partial> = {}): AdminTokenRow { + return { ...makeAdminTokenRecord(overrides), hash: faker.string.hexadecimal({ length: 64 }) } } diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts index 723cbca286..9998ef0a07 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.controller.spec.ts @@ -4,7 +4,7 @@ import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mock } from 'vitest-mock-extended' import { UserGuard } from '../infrastructure/permission/user/user.guard' -import { makeAdminToken } from './admin-token-testing.utils' +import { makeAdminTokenRecord } from './admin-token-testing.utils' import { AdminTokenController } from './admin-token.controller' import { AdminTokenService } from './admin-token.service' @@ -13,7 +13,7 @@ describe('adminTokenController', () => { let controller: AdminTokenController let service: MockProxy - const token = makeAdminToken({ permissions: 0n }) + const token = makeAdminTokenRecord({ permissions: 0n }) beforeEach(async () => { service = mock() diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts index f6846df62d..2037da632c 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.utils.ts @@ -1,12 +1,12 @@ -import type { AdminToken } from './admin-token-queries.utils' +import type { AdminTokenRecord } from './admin-token-queries.utils' -export function toAdminToken({ permissions, ...token }: AdminToken) { +export function toAdminToken({ permissions, ...token }: AdminTokenRecord) { return { ...token, permissions: permissions.toString(), } } -export function toExposedAdminToken(record: AdminToken, password: string) { +export function toExposedAdminToken(record: AdminTokenRecord, password: string) { return { ...toAdminToken(record), password } } From f530b9ee58aa01864640484751c0d720dd3e1247 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:55:31 +0200 Subject: [PATCH 26/27] refactor(admin-token): simplify makeAdminTokenRecord overrides to Partial Co-authored-by: Automata --- .../src/modules/admin-token/admin-token-testing.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index fbfab28fa3..2210cdd30d 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -12,7 +12,7 @@ export function makeAdminTokenOwner(overrides: Partial> & { owner?: AdminTokenRecord['owner'] } = {}): AdminTokenRecord { +export function makeAdminTokenRecord(overrides: Partial = {}): AdminTokenRecord { const owner = overrides.owner ?? makeAdminTokenOwner() return { id: faker.string.uuid(), @@ -30,6 +30,6 @@ export function makeAdminTokenRecord(overrides: Partial> = {}): AdminTokenRow { +export function makeAdminTokenRow(overrides: Partial = {}): AdminTokenRow { return { ...makeAdminTokenRecord(overrides), hash: faker.string.hexadecimal({ length: 64 }) } } From a1d29e60c0410e9c4bd0cf982a26c3f00b4a66ec Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 2 Oct 2026 15:58:39 +0200 Subject: [PATCH 27/27] refactor(admin-token): rename AdminTokenRow to AdminTokenRecordWithHash Co-authored-by: Automata --- .../src/modules/admin-token/admin-token-testing.utils.ts | 4 ++-- .../src/modules/admin-token/admin-token.service.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts index 2210cdd30d..c9858600b6 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token-testing.utils.ts @@ -28,8 +28,8 @@ export function makeAdminTokenRecord(overrides: Partial = {}): } } -export type AdminTokenRow = AdminTokenRecord & { hash: string } +export type AdminTokenRecordWithHash = AdminTokenRecord & { hash: string } -export function makeAdminTokenRow(overrides: Partial = {}): AdminTokenRow { +export function makeAdminTokenRecordWithHash(overrides: Partial = {}): AdminTokenRecordWithHash { return { ...makeAdminTokenRecord(overrides), hash: faker.string.hexadecimal({ length: 64 }) } } diff --git a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts index c258607f71..fde506a8ff 100644 --- a/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts +++ b/apps/server-nestjs/src/modules/admin-token/admin-token.service.spec.ts @@ -6,7 +6,7 @@ import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' import { PrismaService } from '../infrastructure/database/prisma.service' -import { makeAdminTokenRow } from './admin-token-testing.utils' +import { makeAdminTokenRecordWithHash } from './admin-token-testing.utils' import { AdminTokenService } from './admin-token.service' describe('adminTokenService', () => { @@ -29,7 +29,7 @@ describe('adminTokenService', () => { describe('list', () => { it('returns raw token records', async () => { - const token = makeAdminTokenRow() + const token = makeAdminTokenRecordWithHash() prisma.adminToken.findMany.mockResolvedValue([token]) const result = await service.list() @@ -83,7 +83,7 @@ describe('adminTokenService', () => { adminRoleIds: [], type: 'bot', }) - const created = makeAdminTokenRow({ id: tokenId, userId: botUserId, permissions: 2n }) + const created = makeAdminTokenRecordWithHash({ id: tokenId, userId: botUserId, permissions: 2n }) tx.adminToken.create.mockResolvedValue(created) prisma.$transaction.mockImplementation(async fn => fn(tx))