Twee nieuwe GET-route handlers, beide verplicht gescoped op pbiIds (geen
product-brede aanroepen).
- app/api/products/[id]/sprint-membership-summary/route.ts
Response: { [pbiId]: { total, inSprint } } via twee prisma.groupBy calls
(totaal + binnen actieve sprint). Voor state-B tri-state.
- app/api/products/[id]/cross-sprint-blocks/route.ts
Response: { [storyId]: { sprintId, sprintName } } voor stories in andere
OPEN sprints. UX-hint voor disabled-vinkjes; commit-acties blijven
autoritatief.
Tests: 13 cases dekken happy path, 400 zonder pbiIds, 400 zonder sprintId,
404 zonder product-access, auth-fail, en NOT-clause voor excludeSprintId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('@/lib/prisma', () => ({
|
|
prisma: {
|
|
product: { findFirst: vi.fn() },
|
|
story: { groupBy: vi.fn() },
|
|
},
|
|
}))
|
|
|
|
vi.mock('@/lib/api-auth', () => ({
|
|
authenticateApiRequest: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/product-access', () => ({
|
|
productAccessFilter: vi.fn().mockReturnValue({}),
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import { authenticateApiRequest } from '@/lib/api-auth'
|
|
import { GET } from '@/app/api/products/[id]/sprint-membership-summary/route'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
product: { findFirst: ReturnType<typeof vi.fn> }
|
|
story: { groupBy: ReturnType<typeof vi.fn> }
|
|
}
|
|
const mockAuth = authenticateApiRequest as unknown as ReturnType<typeof vi.fn>
|
|
|
|
function makeRequest(url: string) {
|
|
return new Request(url)
|
|
}
|
|
|
|
describe('GET /api/products/[id]/sprint-membership-summary', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockPrisma.product.findFirst.mockReset()
|
|
mockPrisma.story.groupBy.mockReset()
|
|
mockAuth.mockReset().mockResolvedValue({ userId: 'user-1' })
|
|
})
|
|
|
|
it('returns counts per PBI for happy path', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValue({ id: 'p1' })
|
|
mockPrisma.story.groupBy
|
|
.mockResolvedValueOnce([
|
|
{ pbi_id: 'pbiA', _count: { _all: 5 } },
|
|
{ pbi_id: 'pbiB', _count: { _all: 3 } },
|
|
])
|
|
.mockResolvedValueOnce([{ pbi_id: 'pbiA', _count: { _all: 2 } }])
|
|
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1&pbiIds=pbiA,pbiB',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
|
|
expect(res.status).toBe(200)
|
|
const body = await res.json()
|
|
expect(body).toEqual({
|
|
pbiA: { total: 5, inSprint: 2 },
|
|
pbiB: { total: 3, inSprint: 0 },
|
|
})
|
|
})
|
|
|
|
it('rejects when pbiIds is missing', async () => {
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects when pbiIds is empty', async () => {
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1&pbiIds=',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('rejects when sprintId is missing', async () => {
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?pbiIds=pbiA',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('returns 404 when product is not accessible', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValue(null)
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1&pbiIds=pbiA',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
expect(res.status).toBe(404)
|
|
})
|
|
|
|
it('returns auth error when authenticate fails', async () => {
|
|
mockAuth.mockResolvedValue({ error: 'Niet ingelogd', status: 401 })
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1&pbiIds=pbiA',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns zero counts for PBIs without stories', async () => {
|
|
mockPrisma.product.findFirst.mockResolvedValue({ id: 'p1' })
|
|
mockPrisma.story.groupBy
|
|
.mockResolvedValueOnce([])
|
|
.mockResolvedValueOnce([])
|
|
|
|
const req = makeRequest(
|
|
'http://localhost/api/products/p1/sprint-membership-summary?sprintId=sp-1&pbiIds=pbiA,pbiB',
|
|
)
|
|
const res = await GET(req, { params: Promise.resolve({ id: 'p1' }) })
|
|
|
|
const body = await res.json()
|
|
expect(body).toEqual({
|
|
pbiA: { total: 0, inSprint: 0 },
|
|
pbiB: { total: 0, inSprint: 0 },
|
|
})
|
|
})
|
|
})
|