Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion src/app/api/revalidate/route.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @jest-environment node
*/
import { POST } from './route';
import { GET, POST } from './route';
import { revalidatePath, revalidateTag } from 'next/cache';

// Mock Next.js cache
Expand All @@ -15,6 +15,96 @@ jest.mock('../../../i18n/routing', () => ({
AVAILABLE_LOCALES: ['en', 'fr'],
}));

describe('GET /api/revalidate', () => {
const mockRevalidatePath = revalidatePath as jest.MockedFunction<
typeof revalidatePath
>;
const mockRevalidateTag = revalidateTag as jest.MockedFunction<
typeof revalidateTag
>;
const originalEnv = process.env;

beforeEach(() => {
jest.clearAllMocks();
process.env = { ...originalEnv };
});

afterAll(() => {
process.env = originalEnv;
});

it('returns 500 when CRON_SECRET is not configured', async () => {
delete process.env.CRON_SECRET;

const request = new Request('http://localhost:3000/api/revalidate', {
method: 'GET',
headers: {
authorization: 'Bearer some-secret',
},
});

const response = await GET(request);
const json = await response.json();

expect(response.status).toBe(500);
expect(json).toEqual({
ok: false,
error: 'Server misconfigured: CRON_SECRET missing',
});
expect(mockRevalidatePath).not.toHaveBeenCalled();
expect(mockRevalidateTag).not.toHaveBeenCalled();
});

it('returns 401 when authorization header does not match', async () => {
process.env.CRON_SECRET = 'correct-secret';

const request = new Request('http://localhost:3000/api/revalidate', {
method: 'GET',
headers: {
authorization: 'Bearer wrong-secret',
},
});

const response = await GET(request);
const json = await response.json();

expect(response.status).toBe(401);
expect(json).toEqual({
ok: false,
error: 'Unauthorized',
});
expect(mockRevalidatePath).not.toHaveBeenCalled();
expect(mockRevalidateTag).not.toHaveBeenCalled();
});

it('revalidates only GBFS feed pages for authorized cron requests', async () => {
process.env.CRON_SECRET = 'test-secret';

const request = new Request('http://localhost:3000/api/revalidate', {
method: 'GET',
headers: {
authorization: 'Bearer test-secret',
},
});

const response = await GET(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json).toEqual({
ok: true,
message: 'All GBFS feeds revalidated successfully',
});
expect(mockRevalidateTag).toHaveBeenCalledWith('feed-type-gbfs', 'max');
expect(mockRevalidatePath).toHaveBeenCalledWith(
'/[locale]/feeds/gbfs/[feedId]',
'layout',
);
expect(mockRevalidateTag).toHaveBeenCalledTimes(1);
expect(mockRevalidatePath).toHaveBeenCalledTimes(1);
});
});

describe('POST /api/revalidate', () => {
const mockRevalidatePath = revalidatePath as jest.MockedFunction<
typeof revalidatePath
Expand Down
12 changes: 6 additions & 6 deletions src/app/api/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ const defaultRevalidateOptions: RevalidateBody = {
};

/**
* GET handler for the Vercel cron job that revalidates all feed pages once a day.
* GET handler for the Vercel cron job that revalidates all GBFS feed pages.
* Vercel automatically passes Authorization: Bearer <CRON_SECRET> with each invocation.
* Configured in vercel.json under "crons" (schedule: 0 9 * * * = 4am EST / 9am UTC).
* Configured in vercel.json under "crons" for 4am UTC Monday-Saturday and 7am UTC Sunday.
*/
export async function GET(req: Request): Promise<NextResponse> {
const authHeader = req.headers.get('authorization');
Expand All @@ -46,14 +46,14 @@ export async function GET(req: Request): Promise<NextResponse> {
}

try {
revalidateTag('guest-feeds', 'max');
revalidatePath('/[locale]/feeds/[feedDataType]/[feedId]', 'layout');
revalidateTag('feed-type-gbfs', 'max');
revalidatePath('/[locale]/feeds/gbfs/[feedId]', 'layout');
console.log(
'[cron] revalidate /api/revalidate: all-feeds revalidation triggered',
'[cron] revalidate /api/revalidate: all-gbfs-feeds revalidation triggered',
);
return NextResponse.json({
ok: true,
message: 'All feeds revalidated successfully',
message: 'All GBFS feeds revalidated successfully',
});
} catch (error) {
console.error(
Expand Down
6 changes: 5 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"crons": [
{
"path": "/api/revalidate",
"schedule": "0 9 * * *"
"schedule": "0 4 * * 1-6"
},
{
"path": "/api/revalidate",
"schedule": "0 7 * * 0"
}
]
}
Loading