Convert all !isDemo && <Button> patterns to <DemoTooltip show={isDemo}>
<Button disabled={isDemo}> so demo visitors see app capabilities.
Affects: pbi-list, story-panel, story-dialog, task-list, sprint-backlog,
token-manager, product-list, activate-product-button, leave-product-button,
settings page.
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
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<typeof vi.fn>
|
|
findFirst: ReturnType<typeof vi.fn>
|
|
findUnique: ReturnType<typeof vi.fn>
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|