22 vitest cases covering merge semantics (no mutation, array replace, nested merge), Zod schema strictness, server action auth/demo/validation paths, and the optimistic store flow including rollback and demo-mode skip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
vi.mock('next/headers', () => ({ cookies: vi.fn().mockResolvedValue({}) }))
|
|
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/prisma', () => ({
|
|
prisma: {
|
|
user: { findUnique: vi.fn() },
|
|
$transaction: vi.fn(async (fn: (tx: unknown) => Promise<unknown>) => {
|
|
return fn({
|
|
user: {
|
|
findUnique: vi.fn().mockResolvedValue({ settings: {} }),
|
|
update: vi.fn().mockResolvedValue({}),
|
|
},
|
|
})
|
|
}),
|
|
$executeRaw: vi.fn().mockResolvedValue(1),
|
|
},
|
|
}))
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
import { getIronSession } from 'iron-session'
|
|
import { updateUserSettingsAction } from '@/actions/user-settings'
|
|
|
|
const mockPrisma = prisma as unknown as {
|
|
user: { findUnique: ReturnType<typeof vi.fn> }
|
|
$transaction: ReturnType<typeof vi.fn>
|
|
$executeRaw: ReturnType<typeof vi.fn>
|
|
}
|
|
const mockGetIronSession = getIronSession as ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockGetIronSession.mockResolvedValue({ userId: 'user-1', isDemo: false })
|
|
mockPrisma.$executeRaw.mockResolvedValue(1)
|
|
})
|
|
|
|
describe('updateUserSettingsAction', () => {
|
|
it('returns 401 when not logged in', async () => {
|
|
mockGetIronSession.mockResolvedValue({ userId: undefined, isDemo: false })
|
|
const result = await updateUserSettingsAction({})
|
|
expect(result).toEqual({ error: 'Niet ingelogd', code: 401 })
|
|
})
|
|
|
|
it('returns 403 for demo accounts', async () => {
|
|
mockGetIronSession.mockResolvedValue({ userId: 'user-1', isDemo: true })
|
|
const result = await updateUserSettingsAction({})
|
|
expect('error' in result && result.code).toBe(403)
|
|
})
|
|
|
|
it('returns 422 when patch is invalid', async () => {
|
|
const result = await updateUserSettingsAction({
|
|
views: { sprintBacklog: { filterStatus: 'NONSENSE' } },
|
|
} as never)
|
|
expect('error' in result && result.code).toBe(422)
|
|
})
|
|
|
|
it('merges with current settings and emits notify on success', async () => {
|
|
const existingFindUnique = vi.fn().mockResolvedValue({
|
|
settings: { views: { sprintBacklog: { sort: 'code' } } },
|
|
})
|
|
const update = vi.fn().mockResolvedValue({})
|
|
mockPrisma.$transaction.mockImplementationOnce(async (fn: (tx: unknown) => Promise<unknown>) => {
|
|
return fn({ user: { findUnique: existingFindUnique, update } })
|
|
})
|
|
|
|
const result = await updateUserSettingsAction({
|
|
views: { sprintBacklog: { sortDir: 'desc' } },
|
|
})
|
|
|
|
expect('success' in result && result.success).toBe(true)
|
|
expect(update).toHaveBeenCalledWith({
|
|
where: { id: 'user-1' },
|
|
data: { settings: { views: { sprintBacklog: { sort: 'code', sortDir: 'desc' } } } },
|
|
})
|
|
expect(mockPrisma.$executeRaw).toHaveBeenCalled()
|
|
})
|
|
})
|