- Sprint lifecycle: ACTIVE→OPEN, COMPLETED→CLOSED, +ARCHIVED (FAILED behouden) - TaskStatus: +EXCLUDED (overgeslagen door agent-loop via bestaande TO_DO filter) - Cookie-gebaseerde actieve sprint per product (lib/active-sprint.ts) - Route splitsen: /products/[id]/sprint/[sprintId] + /sprint redirect-page - NavBar: gestapelde product/sprint dropdowns + BUILDING-badge derivatie - Backlog selectie-modus + nieuwe-sprint-dialog (createSprintWithPbisAction) - Migratie 20260507210000_sprint_lifecycle: ALTER TYPE RENAME (geen data-rewrite) - Version bump 1.0.0 → 1.2.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
sprint: {
|
|
findFirst: vi.fn(),
|
|
},
|
|
story: {
|
|
findFirst: vi.fn(),
|
|
},
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/lib/api-auth', () => ({
|
|
authenticateApiRequest: vi.fn(),
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import { authenticateApiRequest } from '@/lib/api-auth'
|
|
import { GET as getNextStory } from '@/app/api/products/[id]/next-story/route'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
sprint: { findFirst: ReturnType<typeof vi.fn> }
|
|
story: { findFirst: ReturnType<typeof vi.fn> }
|
|
}
|
|
const mockAuth = authenticateApiRequest as ReturnType<typeof vi.fn>
|
|
|
|
const SPRINT = { id: 'sprint-1', product_id: 'prod-1', status: 'OPEN' }
|
|
const STORY = {
|
|
id: 'story-1',
|
|
title: 'Account aanmaken',
|
|
description: 'Als bezoeker wil ik een account aanmaken.',
|
|
acceptance_criteria: '- Gebruikersnaam verplicht',
|
|
tasks: [
|
|
{ id: 'task-1', title: 'Formulier bouwen', description: null, priority: 1, sort_order: 1, status: 'TO_DO' },
|
|
{ id: 'task-2', title: 'Validatie toevoegen', description: null, priority: 2, sort_order: 2, status: 'TO_DO' },
|
|
],
|
|
}
|
|
|
|
function makeRequest(productId = 'prod-1'): [Request, { params: Promise<{ id: string }> }] {
|
|
return [
|
|
new Request(`http://localhost/api/products/${productId}/next-story`, {
|
|
method: 'GET',
|
|
headers: { Authorization: 'Bearer test-token' },
|
|
}),
|
|
{ params: Promise.resolve({ id: productId }) },
|
|
]
|
|
}
|
|
|
|
describe('GET /api/products/:id/next-story', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockAuth.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
|
})
|
|
|
|
// TC-NS-04
|
|
it('returns 404 when product has no active sprint', async () => {
|
|
mockPrisma.sprint.findFirst.mockResolvedValue(null)
|
|
|
|
const res = await getNextStory(...makeRequest())
|
|
const data = await res.json()
|
|
|
|
expect(res.status).toBe(404)
|
|
expect(data.error).toBeTruthy()
|
|
})
|
|
|
|
// TC-NS-05
|
|
it('returns 404 when active sprint has no IN_SPRINT stories', async () => {
|
|
mockPrisma.sprint.findFirst.mockResolvedValue(SPRINT)
|
|
mockPrisma.story.findFirst.mockResolvedValue(null)
|
|
|
|
const res = await getNextStory(...makeRequest())
|
|
const data = await res.json()
|
|
|
|
expect(res.status).toBe(404)
|
|
expect(data.error).toBeTruthy()
|
|
})
|
|
|
|
// TC-NS-06
|
|
it('returns the highest-priority story with its tasks', async () => {
|
|
mockPrisma.sprint.findFirst.mockResolvedValue(SPRINT)
|
|
mockPrisma.story.findFirst.mockResolvedValue(STORY)
|
|
|
|
const res = await getNextStory(...makeRequest())
|
|
const data = await res.json()
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(data).toMatchObject({
|
|
id: 'story-1',
|
|
title: 'Account aanmaken',
|
|
description: 'Als bezoeker wil ik een account aanmaken.',
|
|
acceptance_criteria: '- Gebruikersnaam verplicht',
|
|
})
|
|
expect(data.tasks).toHaveLength(2)
|
|
expect(data.tasks[0]).toMatchObject({ id: 'task-1', status: 'todo' })
|
|
})
|
|
|
|
it('queries story ordered by priority then sort_order', async () => {
|
|
mockPrisma.sprint.findFirst.mockResolvedValue(SPRINT)
|
|
mockPrisma.story.findFirst.mockResolvedValue(STORY)
|
|
|
|
await getNextStory(...makeRequest())
|
|
|
|
expect(mockPrisma.story.findFirst).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
|
})
|
|
)
|
|
})
|
|
})
|