import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' const mockFetch = vi.fn() vi.stubGlobal('fetch', mockFetch) let revalidateAfterChange: (args: { doc: Record collection: { slug: string } req: object previousDoc: object context: object operation: string }) => unknown let revalidateGlobalAfterChange: (args: { doc: object global: { slug: string } req: object context: object previousDoc: object }) => unknown beforeEach(async () => { vi.resetModules() mockFetch.mockReset() vi.stubEnv('NEXT_PUBLIC_SITE_URL', 'http://localhost:3000') vi.stubEnv('REVALIDATE_SECRET', 'test-revalidate-secret') const mod = await import('@/hooks/revalidatePath') revalidateAfterChange = mod.revalidateAfterChange as typeof revalidateAfterChange revalidateGlobalAfterChange = mod.revalidateGlobalAfterChange as typeof revalidateGlobalAfterChange }) afterEach(() => { vi.restoreAllMocks() }) describe('revalidateAfterChange', () => { it('returns the doc unchanged (fire-and-forget pattern)', () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200 }) const doc = { id: '1', slug: 'about-us' } const result = revalidateAfterChange({ doc, collection: { slug: 'pages' }, req: {}, previousDoc: {}, context: {}, operation: 'update', }) expect(result).toBe(doc) }) it('POSTs to /api/revalidate with slug and collection when doc has slug', async () => { mockFetch.mockResolvedValueOnce({ ok: true }) revalidateAfterChange({ doc: { id: '1', slug: 'about-us' }, collection: { slug: 'pages' }, req: {}, previousDoc: {}, context: {}, operation: 'update', }) await new Promise((r) => setTimeout(r, 0)) expect(mockFetch).toHaveBeenCalledOnce() const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('http://localhost:3000/api/revalidate') expect((init.headers as Record)['Authorization']).toBe( 'Bearer test-revalidate-secret' ) const body = JSON.parse(init.body as string) expect(body).toMatchObject({ slug: 'about-us', collection: 'pages' }) }) it('sends slug as undefined when doc has no slug field', async () => { mockFetch.mockResolvedValueOnce({ ok: true }) revalidateAfterChange({ doc: { id: '2' }, collection: { slug: 'media' }, req: {}, previousDoc: {}, context: {}, operation: 'create', }) await new Promise((r) => setTimeout(r, 0)) const [, init] = mockFetch.mock.calls[0] as [string, RequestInit] const body = JSON.parse(init.body as string) expect(body.collection).toBe('media') // doc.slug is undefined, coerced to undefined in JSON → omitted or undefined expect(body.slug).toBeUndefined() }) it('does not throw when fetch rejects (errors are swallowed)', () => { mockFetch.mockRejectedValueOnce(new Error('network error')) expect(() => revalidateAfterChange({ doc: { id: '1', slug: 'test' }, collection: { slug: 'pages' }, req: {}, previousDoc: {}, context: {}, operation: 'update', }) ).not.toThrow() }) it('does not throw when revalidate endpoint returns non-ok (errors are swallowed)', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 401 }) expect(() => revalidateAfterChange({ doc: { id: '1', slug: 'test' }, collection: { slug: 'pages' }, req: {}, previousDoc: {}, context: {}, operation: 'update', }) ).not.toThrow() }) it('uses NEXT_PUBLIC_SITE_URL env for the request URL', async () => { vi.stubEnv('NEXT_PUBLIC_SITE_URL', 'https://shumiland.ua') vi.resetModules() const mod = await import('@/hooks/revalidatePath') mockFetch.mockResolvedValueOnce({ ok: true }) ;( mod.revalidateAfterChange as typeof revalidateAfterChange )({ doc: { id: '1', slug: 'home' }, collection: { slug: 'pages' }, req: {}, previousDoc: {}, context: {}, operation: 'update', }) await new Promise((r) => setTimeout(r, 0)) expect(mockFetch.mock.calls[0]?.[0]).toBe('https://shumiland.ua/api/revalidate') }) }) describe('revalidateGlobalAfterChange', () => { it('returns the doc unchanged', () => { mockFetch.mockResolvedValueOnce({ ok: true }) const doc = { id: 'g1' } const result = revalidateGlobalAfterChange({ doc, global: { slug: 'header' }, req: {}, context: {}, previousDoc: {}, }) expect(result).toBe(doc) }) it('POSTs to /api/revalidate with global slug', async () => { mockFetch.mockResolvedValueOnce({ ok: true }) revalidateGlobalAfterChange({ doc: {}, global: { slug: 'header' }, req: {}, context: {}, previousDoc: {}, }) await new Promise((r) => setTimeout(r, 0)) const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('http://localhost:3000/api/revalidate') const body = JSON.parse(init.body as string) expect(body).toEqual({ global: 'header' }) }) it('does not throw when fetch rejects', () => { mockFetch.mockRejectedValueOnce(new Error('timeout')) expect(() => revalidateGlobalAfterChange({ doc: {}, global: { slug: 'footer' }, req: {}, context: {}, previousDoc: {}, }) ).not.toThrow() }) })