import { Prisma, Role } from '@prisma/client'; import { GLOBAL_LT_KC_ID, resolveOrProvisionAuthentikUser, toAuthenticatedUser, } from './provision-user'; const CLAIMS = { sub: 'sub-1', email: 'New.Person@Example.org', firstName: 'New', lastName: 'Person', }; function p2002() { return new Prisma.PrismaClientKnownRequestError('unique', { code: 'P2002', clientVersion: 'test', }); } describe('resolveOrProvisionAuthentikUser', () => { it('returns the existing user without creating or capturing when nothing changed', async () => { const sync = { capture: jest.fn() }; const existing = { id: 'u-1', authentikSub: 'sub-1', isLeitungsteam: false, memberships: [] }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(existing), create: jest.fn(), update: jest.fn(), }, }; const res = await resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false); expect(res).toBe(existing); expect(prisma.user.create).not.toHaveBeenCalled(); expect(prisma.user.update).not.toHaveBeenCalled(); expect(sync.capture).not.toHaveBeenCalled(); }); it('provisions a new user from claims (lowercased email) and captures it', async () => { const sync = { capture: jest.fn().mockResolvedValue(undefined) }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(null), create: jest.fn(({ data }: { data: Record }) => Promise.resolve({ id: 'u-2', isLeitungsteam: false, ...data }), ), update: jest.fn(), }, }; const res = await resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false); expect(prisma.user.create).toHaveBeenCalledWith({ data: { authentikSub: 'sub-1', email: 'new.person@example.org', firstName: 'New', lastName: 'Person', }, }); expect(res.memberships).toEqual([]); expect(sync.capture).toHaveBeenCalledWith('User', 'CREATE', 'u-2', expect.anything()); }); it('reconciles the LEITUNGSTEAM flag up when the token now has the group', async () => { const sync = { capture: jest.fn().mockResolvedValue(undefined) }; const existing = { id: 'u-1', authentikSub: 'sub-1', isLeitungsteam: false, memberships: [] }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(existing), create: jest.fn(), update: jest.fn(({ data }: { data: Record }) => Promise.resolve({ ...existing, ...data, memberships: [] }), ), }, }; const res = await resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, true); expect(prisma.user.update).toHaveBeenCalledWith({ where: { id: 'u-1' }, data: { isLeitungsteam: true }, include: { memberships: { where: { status: 'ACTIVE' } } }, }); expect(res.isLeitungsteam).toBe(true); expect(sync.capture).toHaveBeenCalledWith('User', 'UPDATE', 'u-1', expect.anything()); }); it('reconciles the LEITUNGSTEAM flag down when the group is gone', async () => { const sync = { capture: jest.fn().mockResolvedValue(undefined) }; const existing = { id: 'u-1', authentikSub: 'sub-1', isLeitungsteam: true, memberships: [] }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(existing), create: jest.fn(), update: jest.fn(({ data }: { data: Record }) => Promise.resolve({ ...existing, ...data, memberships: [] }), ), }, }; await resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false); expect(prisma.user.update).toHaveBeenCalledWith( expect.objectContaining({ data: { isLeitungsteam: false } }), ); }); it('recovers from a concurrent-create race (P2002) by re-reading', async () => { const sync = { capture: jest.fn() }; const raced = { id: 'u-3', authentikSub: 'sub-1', isLeitungsteam: false, memberships: [] }; const prisma = { user: { findUnique: jest.fn().mockResolvedValueOnce(null).mockResolvedValueOnce(raced), create: jest.fn().mockRejectedValue(p2002()), update: jest.fn(), }, }; const res = await resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false); expect(res).toBe(raced); expect(sync.capture).not.toHaveBeenCalled(); }); it('rethrows a P2002 when the row still cannot be found', async () => { const sync = { capture: jest.fn() }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(null), create: jest.fn().mockRejectedValue(p2002()), update: jest.fn(), }, }; await expect( resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false), ).rejects.toBeInstanceOf(Prisma.PrismaClientKnownRequestError); }); it('rethrows a non-P2002 error', async () => { const sync = { capture: jest.fn() }; const prisma = { user: { findUnique: jest.fn().mockResolvedValue(null), create: jest.fn().mockRejectedValue(new Error('db down')), update: jest.fn(), }, }; await expect( resolveOrProvisionAuthentikUser(prisma as never, sync as never, CLAIMS, false), ).rejects.toThrow('db down'); }); }); describe('toAuthenticatedUser', () => { const row = { id: 'u-1', authentikSub: 'sub-1', email: 'a@b.org', isLeitungsteam: false, memberships: [ { kcId: 'kc-1', gemeindeId: 'gem-1', role: Role.GEMEINDE_VERANTWORTLICHER }, ], }; it('maps membership rows straight through when not Leitungsteam', () => { const res = toAuthenticatedUser(row as never); expect(res.memberships).toEqual([ { kcId: 'kc-1', gemeindeId: 'gem-1', role: Role.GEMEINDE_VERANTWORTLICHER }, ]); }); it('prepends a synthetic global LEITUNGSTEAM membership when the flag is set', () => { const res = toAuthenticatedUser({ ...row, isLeitungsteam: true } as never); expect(res.memberships[0]).toEqual({ kcId: GLOBAL_LT_KC_ID, gemeindeId: null, role: Role.LEITUNGSTEAM, }); expect(res.memberships).toHaveLength(2); }); });