import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockReadPairCookie, mockClearPairCookie, mockSession, mockGetIronSession } = vi.hoisted( () => ({ mockReadPairCookie: vi.fn(), mockClearPairCookie: vi.fn(), mockSession: { userId: '', isDemo: false, paired: false, pairedExpiresAt: 0, save: vi.fn() }, mockGetIronSession: vi.fn(), }), ) vi.mock('@/lib/auth/pair-cookie', () => ({ readPairCookie: mockReadPairCookie, clearPairCookie: mockClearPairCookie, setPairCookie: vi.fn(), })) vi.mock('iron-session', () => ({ getIronSession: mockGetIronSession, })) vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}), })) vi.mock('@/lib/prisma', () => ({ prisma: { loginPairing: { updateMany: vi.fn(), findFirst: vi.fn(), findUnique: vi.fn(), }, }, })) import { prisma } from '@/lib/prisma' import { hashToken } from '@/lib/auth/pairing' import { POST } from '@/app/api/auth/pair/claim/route' const mockPrisma = prisma as unknown as { loginPairing: { updateMany: ReturnType findFirst: ReturnType findUnique: ReturnType } } const COOKIE_TOKEN = 'desktop-token-abc' const COOKIE_HASH = hashToken(COOKIE_TOKEN) const PAIRING_ID = 'cmohmk0qpair006c001' function makePost(body: unknown): Request { return new Request('http://localhost:3000/api/auth/pair/claim', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: typeof body === 'string' ? body : JSON.stringify(body), }) } beforeEach(() => { vi.clearAllMocks() // Reset session-mock voor elke test mockSession.userId = '' mockSession.isDemo = false mockSession.paired = false mockSession.pairedExpiresAt = 0 mockSession.save = vi.fn().mockResolvedValue(undefined) mockGetIronSession.mockResolvedValue(mockSession) }) describe('POST /api/auth/pair/claim', () => { it('200: schrijft iron-session, clear s4m_pair, retourneert {ok:true}', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) mockPrisma.loginPairing.updateMany.mockResolvedValue({ count: 1 }) mockPrisma.loginPairing.findUnique.mockResolvedValue({ user_id: 'user-42', user: { is_demo: false }, }) const res = await POST(makePost({ pairingId: PAIRING_ID })) expect(res.status).toBe(200) expect(await res.json()).toEqual({ ok: true }) // Atomic update aangeroepen met juiste WHERE expect(mockPrisma.loginPairing.updateMany).toHaveBeenCalledTimes(1) const where = mockPrisma.loginPairing.updateMany.mock.calls[0][0].where expect(where).toMatchObject({ id: PAIRING_ID, status: 'approved', desktop_token_hash: COOKIE_HASH, }) expect(where.expires_at).toMatchObject({ gt: expect.any(Date) }) // Iron-session payload expect(mockSession.userId).toBe('user-42') expect(mockSession.isDemo).toBe(false) expect(mockSession.paired).toBe(true) const dt = mockSession.pairedExpiresAt - Date.now() expect(dt).toBeGreaterThan(8 * 60 * 60 * 1000 - 5_000) expect(dt).toBeLessThan(8 * 60 * 60 * 1000 + 5_000) expect(mockSession.save).toHaveBeenCalledTimes(1) expect(mockClearPairCookie).toHaveBeenCalledTimes(1) }) it('demo-user: claim geblokkeerd met 403 (ST-1110.4)', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) mockPrisma.loginPairing.updateMany.mockResolvedValue({ count: 1 }) mockPrisma.loginPairing.findUnique.mockResolvedValue({ user_id: 'demo-1', user: { is_demo: true }, }) const res = await POST(makePost({ pairingId: PAIRING_ID })) expect(res.status).toBe(403) const body = await res.json() expect(body.error).toMatch(/demo-modus/i) expect(mockClearPairCookie).toHaveBeenCalledTimes(1) }) it('401 zonder s4m_pair-cookie', async () => { mockReadPairCookie.mockResolvedValue(null) const res = await POST(makePost({ pairingId: PAIRING_ID })) expect(res.status).toBe(401) expect(mockPrisma.loginPairing.updateMany).not.toHaveBeenCalled() }) it('400 zonder body', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) const res = await POST(makePost('not-json')) expect(res.status).toBe(400) }) it('400 zonder pairingId', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) const res = await POST(makePost({})) expect(res.status).toBe(400) }) it('410 op tweede claim — pairing al consumed', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) mockPrisma.loginPairing.updateMany.mockResolvedValue({ count: 0 }) mockPrisma.loginPairing.findFirst.mockResolvedValue({ status: 'consumed' }) const res = await POST(makePost({ pairingId: PAIRING_ID })) expect(res.status).toBe(410) expect(mockClearPairCookie).toHaveBeenCalledTimes(1) expect(mockSession.save).not.toHaveBeenCalled() }) it('401 op cookie/hash-mismatch (pairing bestaat niet voor deze cookie)', async () => { mockReadPairCookie.mockResolvedValue(COOKIE_TOKEN) mockPrisma.loginPairing.updateMany.mockResolvedValue({ count: 0 }) mockPrisma.loginPairing.findFirst.mockResolvedValue(null) const res = await POST(makePost({ pairingId: PAIRING_ID })) expect(res.status).toBe(401) expect(mockClearPairCookie).toHaveBeenCalledTimes(1) }) })