- lib/user-settings.ts: activeSprints values nullable in Zod-schema. Key-aanwezigheid heeft nu betekenis (key+null = bewust geen sprint; key ontbreekt = fallback-cascade). - lib/active-sprint.ts: nieuwe readStoredActiveSprintState helper + resolveActiveSprint respecteert expliciet 'cleared' state zonder fallback. clearActiveSprintInSettings schrijft null i.p.v. de key te verwijderen. - actions/active-sprint.ts: nieuwe clearActiveSprintAction met auth + membership-check. - components/shared/sprint-switcher.tsx: '— Geen actieve sprint —'-optie in dropdown, disabled wanneer er geen actieve sprint is. - Tests: nieuwe active-sprint.test.ts (resolver-paden + clear), active-sprint-action.test.ts (action-laag), uitbreiding user-settings.test.ts. Plan: docs/plans/PBI-79-backlog-sprint-workflow.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3 KiB
TypeScript
103 lines
3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
|
|
vi.mock('next/headers', () => ({
|
|
cookies: vi.fn().mockResolvedValue({
|
|
set: vi.fn(),
|
|
get: vi.fn(),
|
|
delete: vi.fn(),
|
|
}),
|
|
}))
|
|
vi.mock('iron-session', () => ({
|
|
getIronSession: vi.fn().mockResolvedValue({ userId: 'user-1', isDemo: false }),
|
|
}))
|
|
vi.mock('@/lib/session', () => ({
|
|
sessionOptions: { cookieName: 'test', password: 'test' },
|
|
}))
|
|
vi.mock('@/lib/product-access', () => ({
|
|
productAccessFilter: vi.fn().mockReturnValue({}),
|
|
}))
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
sprint: { findFirst: vi.fn() },
|
|
product: { findFirst: vi.fn() },
|
|
user: {
|
|
findUnique: vi.fn(),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
$executeRaw: vi.fn().mockResolvedValue(1),
|
|
},
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import { clearActiveSprintAction } from '@/actions/active-sprint'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
product: { findFirst: ReturnType<typeof vi.fn> }
|
|
user: {
|
|
findUnique: ReturnType<typeof vi.fn>
|
|
update: ReturnType<typeof vi.fn>
|
|
}
|
|
}
|
|
|
|
describe('clearActiveSprintAction', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('writes null instead of deleting the key', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValueOnce({ id: 'p1' })
|
|
mockPrisma.user.findUnique.mockResolvedValueOnce({
|
|
settings: { layout: { activeSprints: { p1: 'sprint-1', p2: 'sprint-2' } } },
|
|
})
|
|
|
|
const result = await clearActiveSprintAction('p1')
|
|
|
|
expect(result).toEqual({ success: true })
|
|
const updateArg = mockPrisma.user.update.mock.calls[0][0] as {
|
|
data: { settings: { layout?: { activeSprints?: Record<string, string | null> } } }
|
|
}
|
|
expect(updateArg.data.settings.layout?.activeSprints).toEqual({
|
|
p1: null,
|
|
p2: 'sprint-2',
|
|
})
|
|
})
|
|
|
|
it('preserves other product keys when clearing one', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValueOnce({ id: 'p1' })
|
|
mockPrisma.user.findUnique.mockResolvedValueOnce({
|
|
settings: {
|
|
layout: {
|
|
activeSprints: { p1: 'sprint-1', p2: 'sprint-2', p3: null },
|
|
},
|
|
},
|
|
})
|
|
|
|
await clearActiveSprintAction('p1')
|
|
|
|
const updateArg = mockPrisma.user.update.mock.calls[0][0] as {
|
|
data: { settings: { layout?: { activeSprints?: Record<string, string | null> } } }
|
|
}
|
|
expect(updateArg.data.settings.layout?.activeSprints).toEqual({
|
|
p1: null,
|
|
p2: 'sprint-2',
|
|
p3: null,
|
|
})
|
|
})
|
|
|
|
it('rejects when product is not accessible', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValueOnce(null)
|
|
|
|
const result = await clearActiveSprintAction('p1')
|
|
|
|
expect(result).toEqual({ error: 'Product niet gevonden of niet toegankelijk' })
|
|
expect(mockPrisma.user.update).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects invalid productId', async () => {
|
|
const result = await clearActiveSprintAction('')
|
|
|
|
expect(result).toEqual({ error: 'Ongeldig product-id' })
|
|
expect(mockPrisma.user.update).not.toHaveBeenCalled()
|
|
})
|
|
})
|