109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { POST } from '#app/api/revalidate/route';
|
|
|
|
const { revalidateTag } = vi.hoisted(() => ({ revalidateTag: vi.fn() }));
|
|
|
|
vi.mock('next/cache', () => ({ revalidateTag }));
|
|
|
|
const SECRET = 'test-revalidate-secret';
|
|
|
|
function request({
|
|
secret = SECRET,
|
|
body = JSON.stringify({ tags: ['web-manager'] })
|
|
}: { secret?: string | null; body?: string } = {}) {
|
|
const headers = new Headers({ 'content-type': 'application/json' });
|
|
if (secret !== null) {
|
|
headers.set('x-revalidate-secret', secret);
|
|
}
|
|
|
|
return new Request('http://web.test/api/revalidate', { method: 'POST', headers, body });
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.stubEnv('REVALIDATE_SECRET', SECRET);
|
|
revalidateTag.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('authentication', () => {
|
|
it('rejects a request with no secret header', async () => {
|
|
const response = await POST(request({ secret: null }));
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(revalidateTag).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects a wrong secret', async () => {
|
|
const response = await POST(request({ secret: 'wrong' }));
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(revalidateTag).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects everything when the server has no secret configured — even a matching empty header', async () => {
|
|
vi.stubEnv('REVALIDATE_SECRET', '');
|
|
|
|
expect((await POST(request({ secret: '' }))).status).toBe(401);
|
|
expect((await POST(request())).status).toBe(401);
|
|
expect(revalidateTag).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('payload validation', () => {
|
|
it('rejects a body that is not JSON', async () => {
|
|
const response = await POST(request({ body: 'not json {' }));
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(await response.json()).toEqual({ message: 'Invalid JSON body' });
|
|
});
|
|
|
|
it.each([
|
|
['a non-object payload', JSON.stringify('tags')],
|
|
['a null payload', JSON.stringify(null)],
|
|
['a missing tags field', JSON.stringify({})],
|
|
['tags that are not an array', JSON.stringify({ tags: 'web-manager' })],
|
|
['an empty tags array', JSON.stringify({ tags: [] })],
|
|
['a non-string tag', JSON.stringify({ tags: ['ok', 7] })],
|
|
['an empty-string tag', JSON.stringify({ tags: [''] })]
|
|
])('rejects %s with 400', async (_label, body) => {
|
|
const response = await POST(request({ body }));
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(revalidateTag).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('accepts exactly 50 tags but rejects 51', async () => {
|
|
const fifty = Array.from({ length: 50 }, (_, i) => `tag-${i}`);
|
|
|
|
expect((await POST(request({ body: JSON.stringify({ tags: fifty }) }))).status).toBe(200);
|
|
expect(
|
|
(await POST(request({ body: JSON.stringify({ tags: [...fifty, 'one-more'] }) }))).status
|
|
).toBe(400);
|
|
});
|
|
|
|
it('accepts a 256-character tag but rejects 257', async () => {
|
|
expect(
|
|
(await POST(request({ body: JSON.stringify({ tags: ['a'.repeat(256)] }) }))).status
|
|
).toBe(200);
|
|
expect(
|
|
(await POST(request({ body: JSON.stringify({ tags: ['a'.repeat(257)] }) }))).status
|
|
).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('revalidation', () => {
|
|
it('revalidates every tag with the max profile and echoes them back', async () => {
|
|
const tags = ['web-manager', 'web-manager:page:en:about'];
|
|
const response = await POST(request({ body: JSON.stringify({ tags }) }));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(await response.json()).toEqual({ revalidated: tags });
|
|
expect(revalidateTag).toHaveBeenCalledTimes(2);
|
|
expect(revalidateTag).toHaveBeenNthCalledWith(1, 'web-manager', 'max');
|
|
expect(revalidateTag).toHaveBeenNthCalledWith(2, 'web-manager:page:en:about', 'max');
|
|
});
|
|
});
|