test(PBI-79/ST-1344): updateSprintAction regression coverage

Audits van de geplande non-regressie-tests laten zien dat alle invarianten
uit het ST-1344 plan reeds gedekt zijn door eerder toegevoegde tests:

- clearActiveSprintAction null-not-delete → __tests__/lib/active-sprint.test.ts
  + __tests__/actions/active-sprint-action.test.ts
- Endpoints rejecten zonder pbiIds (400) → __tests__/api/sprint-membership-summary.test.ts
  + __tests__/api/cross-sprint-blocks.test.ts
- Status-mutaties story.status=IN_SPRINT/OPEN met task.sprint_id cascade
  in dezelfde transactie → __tests__/actions/create-sprint-with-selection.test.ts
  + __tests__/actions/commit-sprint-membership.test.ts
- Cross-sprint conflicts + DONE-eligibility → __tests__/lib/sprint-conflicts.test.ts

Nieuw: __tests__/actions/update-sprint.test.ts (6 cases) dekt
updateSprintAction die nog geen tests had — goal alleen, dates alleen,
null-clear, 403 zonder access, lege goal weigering, leeg fields-object
weigering.

Handmatige E2E checklist (T-949) blijft staan voor menselijke browser-
validatie tijdens PR-review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-11 17:12:28 +02:00
parent b91d92a02d
commit 0c36f4e4a1

View file

@ -0,0 +1,148 @@
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({}),
getAccessibleProduct: vi.fn().mockResolvedValue({ id: 'product-1' }),
}))
vi.mock('@/lib/rate-limit', () => ({
enforceUserRateLimit: vi.fn().mockReturnValue(null),
}))
vi.mock('@/lib/code-server', () => ({
createWithCodeRetry: vi.fn(),
generateNextSprintCode: vi.fn(),
}))
vi.mock('@/lib/active-sprint', () => ({
setActiveSprintInSettings: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/prisma', () => ({
prisma: {
sprint: {
findFirst: vi.fn(),
update: vi.fn(),
},
story: {
findMany: vi.fn(),
updateMany: vi.fn(),
},
task: {
findMany: vi.fn(),
updateMany: vi.fn(),
},
$transaction: vi.fn(),
},
}))
import { prisma } from '@/lib/prisma'
import { updateSprintAction } from '@/actions/sprints'
type Mocked = {
sprint: {
findFirst: ReturnType<typeof vi.fn>
update: ReturnType<typeof vi.fn>
}
}
const mockPrisma = prisma as unknown as Mocked
beforeEach(() => {
vi.clearAllMocks()
mockPrisma.sprint.findFirst.mockReset().mockResolvedValue({
id: 'sprint-1',
product_id: 'product-1',
})
mockPrisma.sprint.update.mockReset().mockResolvedValue({})
})
describe('updateSprintAction', () => {
it('updates sprint_goal alone', async () => {
const result = await updateSprintAction({
sprintId: 'sprint-1',
fields: { goal: 'Nieuw doel' },
})
expect('success' in result).toBe(true)
expect(mockPrisma.sprint.update).toHaveBeenCalledWith({
where: { id: 'sprint-1' },
data: { sprint_goal: 'Nieuw doel' },
})
})
it('updates dates only', async () => {
await updateSprintAction({
sprintId: 'sprint-1',
fields: { startAt: '2026-06-01', endAt: '2026-06-14' },
})
expect(mockPrisma.sprint.update).toHaveBeenCalledWith({
where: { id: 'sprint-1' },
data: {
start_date: new Date('2026-06-01'),
end_date: new Date('2026-06-14'),
},
})
})
it('accepts null to clear a date', async () => {
await updateSprintAction({
sprintId: 'sprint-1',
fields: { startAt: null },
})
expect(mockPrisma.sprint.update).toHaveBeenCalledWith({
where: { id: 'sprint-1' },
data: { start_date: null },
})
})
it('rejects when sprint not accessible', async () => {
mockPrisma.sprint.findFirst.mockResolvedValue(null)
const result = await updateSprintAction({
sprintId: 'sprint-1',
fields: { goal: 'x' },
})
expect('error' in result).toBe(true)
if ('error' in result) {
expect(result.code).toBe(403)
}
expect(mockPrisma.sprint.update).not.toHaveBeenCalled()
})
it('rejects empty goal', async () => {
const result = await updateSprintAction({
sprintId: 'sprint-1',
fields: { goal: '' },
})
expect('error' in result).toBe(true)
expect(mockPrisma.sprint.update).not.toHaveBeenCalled()
})
it('rejects when no fields are supplied', async () => {
const result = await updateSprintAction({
sprintId: 'sprint-1',
fields: {},
})
// Schema-refine should reject; OR action treats empty data as no-op success.
// Current implementation: refine forces minstens één veld → 422 error.
expect('error' in result).toBe(true)
if ('error' in result) {
expect(result.code).toBe(422)
}
})
})