- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest - B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks - B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting - B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth - B5: 13 test files covering unit libs and all API routes - B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint - B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { NextRequest } from 'next/server'
|
|
|
|
const mockRevalidatePath = vi.fn()
|
|
vi.mock('next/cache', () => ({
|
|
revalidatePath: mockRevalidatePath,
|
|
revalidateTag: vi.fn(),
|
|
}))
|
|
|
|
const SECRET = 'test-revalidate-secret' // matches vitest.setup.ts
|
|
|
|
function makeRequest(body: unknown, token = SECRET): NextRequest {
|
|
return new NextRequest('http://localhost/api/revalidate', {
|
|
method: 'POST',
|
|
body: typeof body === 'string' ? body : JSON.stringify(body),
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
}
|
|
|
|
let POST: (req: NextRequest) => Promise<Response>
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
mockRevalidatePath.mockReset()
|
|
vi.stubEnv('REVALIDATE_SECRET', SECRET)
|
|
const mod = await import('@/app/api/revalidate/route')
|
|
POST = mod.POST
|
|
})
|
|
|
|
describe('POST /api/revalidate', () => {
|
|
it('returns 401 when authorization header is missing', async () => {
|
|
const req = new NextRequest('http://localhost/api/revalidate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ slug: 'home' }),
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
const res = await POST(req)
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 401 for wrong token', async () => {
|
|
const res = await POST(makeRequest({ slug: 'home' }, 'wrong-token'))
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 401 when REVALIDATE_SECRET env is empty (no bypass)', async () => {
|
|
vi.stubEnv('REVALIDATE_SECRET', '')
|
|
vi.resetModules()
|
|
const mod = await import('@/app/api/revalidate/route')
|
|
const res = await mod.POST(makeRequest({ slug: 'home' }, ''))
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
it('returns 400 for invalid JSON body', async () => {
|
|
const req = new NextRequest('http://localhost/api/revalidate', {
|
|
method: 'POST',
|
|
body: 'not-json',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: `Bearer ${SECRET}`,
|
|
},
|
|
})
|
|
const res = await POST(req)
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('revalidates a specific path when slug is provided', async () => {
|
|
const res = await POST(makeRequest({ slug: 'about-us' }))
|
|
expect(res.status).toBe(200)
|
|
expect(mockRevalidatePath).toHaveBeenCalledWith('/about-us')
|
|
expect((await res.json()).revalidated).toBe(true)
|
|
})
|
|
|
|
it('revalidates layout when global is provided', async () => {
|
|
const res = await POST(makeRequest({ global: 'header' }))
|
|
expect(res.status).toBe(200)
|
|
expect(mockRevalidatePath).toHaveBeenCalledWith('/', 'layout')
|
|
})
|
|
|
|
it('revalidates layout when collection is provided', async () => {
|
|
const res = await POST(makeRequest({ collection: 'posts' }))
|
|
expect(res.status).toBe(200)
|
|
expect(mockRevalidatePath).toHaveBeenCalledWith('/', 'layout')
|
|
})
|
|
|
|
it('returns revalidated:true and calls no revalidation when body has none of slug/collection/global', async () => {
|
|
const res = await POST(makeRequest({}))
|
|
expect(res.status).toBe(200)
|
|
expect(mockRevalidatePath).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns 500 when revalidatePath throws', async () => {
|
|
mockRevalidatePath.mockImplementationOnce(() => {
|
|
throw new Error('cache error')
|
|
})
|
|
const res = await POST(makeRequest({ slug: 'boom' }))
|
|
expect(res.status).toBe(500)
|
|
})
|
|
})
|