import type { Logger } from 'nestjs-pino'; import type { PrismaService } from 'nestjs-prisma'; import { UserDirectoryService, type UserDirectoryEntry } from './user-directory.service'; interface PrismaStub { user: { upsert: jest.Mock; }; } function makeLogger() { return { log: jest.fn(), warn: jest.fn(), error: jest.fn() } as unknown as Logger & { log: jest.Mock; warn: jest.Mock; error: jest.Mock; }; } function makeSubject(opts?: { upsert?: jest.Mock }): { service: UserDirectoryService; prisma: PrismaStub; logger: ReturnType; } { const prisma: PrismaStub = { user: { upsert: opts?.upsert ?? jest.fn().mockResolvedValue(undefined), }, }; const logger = makeLogger(); const service = new UserDirectoryService(prisma as unknown as PrismaService, logger); return { service, prisma, logger }; } const ENTRY: UserDirectoryEntry = { oid: 'user-oid', tid: 'tenant-1', username: 'jane.doe@apf.example', displayName: 'Jane Doe', audience: 'workforce', }; describe('UserDirectoryService.recordSignIn', () => { it('issues an upsert keyed by oid with the right create / update payloads', async () => { const { service, prisma } = makeSubject(); await service.recordSignIn(ENTRY); expect(prisma.user.upsert).toHaveBeenCalledTimes(1); const args = prisma.user.upsert.mock.calls[0]?.[0] as { where: { oid: string }; create: Record; update: Record; }; expect(args.where).toEqual({ oid: 'user-oid' }); expect(args.create).toEqual({ oid: 'user-oid', tid: 'tenant-1', audience: 'workforce', username: 'jane.doe@apf.example', displayName: 'Jane Doe', }); // Update path: refreshes every mutable field + bumps lastSeenAt; // crucially does NOT include `firstSeenAt` so it stays // immutable after first sign-in. expect(args.update).toEqual( expect.objectContaining({ tid: 'tenant-1', audience: 'workforce', username: 'jane.doe@apf.example', displayName: 'Jane Doe', }), ); expect(args.update['firstSeenAt']).toBeUndefined(); expect(args.update['lastSeenAt']).toBeInstanceOf(Date); }); it('updates `displayName` + `username` if either changed since the previous sign-in', async () => { const { service, prisma } = makeSubject(); await service.recordSignIn({ ...ENTRY, displayName: 'Jane Updated', username: 'jane2@apf.example', }); const args = prisma.user.upsert.mock.calls[0]?.[0] as { update: Record; }; expect(args.update['displayName']).toBe('Jane Updated'); expect(args.update['username']).toBe('jane2@apf.example'); }); it('swallows Prisma errors and logs them — never propagates (sign-in must succeed)', async () => { const upsert = jest.fn().mockRejectedValue(new Error('connection refused')); const { service, logger } = makeSubject({ upsert }); await expect(service.recordSignIn(ENTRY)).resolves.toBeUndefined(); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ event: 'user_directory.record_sign_in_failed', oid: 'user-oid', reason: 'connection refused', }), 'UserDirectoryService', ); }); it('logs the underlying message verbatim when the rejection is not an Error', async () => { const upsert = jest.fn().mockRejectedValue('some string'); const { service, logger } = makeSubject({ upsert }); await service.recordSignIn(ENTRY); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ reason: 'some string' }), 'UserDirectoryService', ); }); });