import { Test } from '@nestjs/testing'; import { PrismaService } from 'nestjs-prisma'; import { AdminUsersReader } from './admin-users-reader.service'; interface MockPrisma { user: { count: jest.Mock; findMany: jest.Mock; }; $transaction: jest.Mock; } function buildPrisma(opts?: { count?: number; items?: unknown[] }): MockPrisma { const count = jest.fn().mockResolvedValue(opts?.count ?? 0); const findMany = jest.fn().mockResolvedValue(opts?.items ?? []); return { user: { count, findMany }, // The reader calls $transaction with the promises returned by // `count()` + `findMany()` — the operations have already fired // by the time $transaction sees them. The mock just resolves // them together so Prisma's tuple-return contract is honoured. $transaction: jest .fn() .mockImplementation((promises: ReadonlyArray>) => Promise.all(promises)), }; } async function createSubject(prisma: MockPrisma): Promise { const moduleRef = await Test.createTestingModule({ providers: [AdminUsersReader, { provide: PrismaService, useValue: prisma }], }).compile(); return moduleRef.get(AdminUsersReader); } const ROW = { oid: 'user-oid', tid: 'tenant-1', audience: 'workforce', username: 'jane.doe@apf.example', displayName: 'Jane Doe', firstSeenAt: new Date('2026-05-10T10:00:00.000Z'), lastSeenAt: new Date('2026-05-14T08:30:00.000Z'), }; describe('AdminUsersReader.findUsers', () => { it('issues a COUNT + SELECT in a single Prisma transaction', async () => { const prisma = buildPrisma({ count: 5, items: [ROW] }); const reader = await createSubject(prisma); const page = await reader.findUsers({}); expect(prisma.$transaction).toHaveBeenCalledTimes(1); expect(prisma.user.count).toHaveBeenCalledTimes(1); expect(prisma.user.findMany).toHaveBeenCalledTimes(1); expect(page.total).toBe(5); expect(page.items).toHaveLength(1); }); it('projects rows into the SPA-facing shape (ISO timestamps)', async () => { const prisma = buildPrisma({ items: [ROW] }); const reader = await createSubject(prisma); const page = await reader.findUsers({}); expect(page.items[0]).toEqual({ oid: 'user-oid', tid: 'tenant-1', audience: 'workforce', username: 'jane.doe@apf.example', displayName: 'Jane Doe', firstSeenAt: '2026-05-10T10:00:00.000Z', lastSeenAt: '2026-05-14T08:30:00.000Z', }); }); it('orders by last_seen_at DESC, oid ASC for deterministic pagination', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({}); const findManyArgs = prisma.user.findMany.mock.calls[0]?.[0] as { orderBy: ReadonlyArray>; }; expect(findManyArgs.orderBy).toEqual([{ lastSeenAt: 'desc' }, { oid: 'asc' }]); }); it('passes startsWith filter on username through to Prisma', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({ username: 'jane' }); const args = prisma.user.findMany.mock.calls[0]?.[0] as { where: { username?: { startsWith?: string } }; }; expect(args.where.username).toEqual({ startsWith: 'jane' }); }); it('passes case-insensitive contains filter on displayName through to Prisma', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({ displayName: 'doe' }); const args = prisma.user.findMany.mock.calls[0]?.[0] as { where: { displayName?: { contains?: string; mode?: string } }; }; expect(args.where.displayName).toEqual({ contains: 'doe', mode: 'insensitive' }); }); it('combines lastSeenAtFrom and lastSeenAtTo into a single gte/lt range filter', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({ lastSeenAtFrom: '2026-05-01T00:00:00.000Z', lastSeenAtTo: '2026-05-14T23:59:59.999Z', }); const args = prisma.user.findMany.mock.calls[0]?.[0] as { where: { lastSeenAt?: { gte?: Date; lt?: Date } }; }; expect(args.where.lastSeenAt?.gte).toBeInstanceOf(Date); expect(args.where.lastSeenAt?.lt).toBeInstanceOf(Date); expect(args.where.lastSeenAt?.gte?.toISOString()).toBe('2026-05-01T00:00:00.000Z'); expect(args.where.lastSeenAt?.lt?.toISOString()).toBe('2026-05-14T23:59:59.999Z'); }); it('applies default limit (50) and offset (0) when not provided', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); const page = await reader.findUsers({}); const args = prisma.user.findMany.mock.calls[0]?.[0] as { take: number; skip: number; }; expect(args.take).toBe(50); expect(args.skip).toBe(0); expect(page.limit).toBe(50); expect(page.offset).toBe(0); }); it('caps limit at MAX_LIMIT (200) even when a caller bypasses the DTO', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({ limit: 1000 }); const args = prisma.user.findMany.mock.calls[0]?.[0] as { take: number }; expect(args.take).toBe(200); }); it('emits no WHERE clause when no filter is provided', async () => { const prisma = buildPrisma(); const reader = await createSubject(prisma); await reader.findUsers({}); const args = prisma.user.findMany.mock.calls[0]?.[0] as { where: object }; expect(args.where).toEqual({}); }); });