import { Request, Response } from 'node-fetch'; import mockDate from 'mockdate'; import swr, { CACHE_CONTROL_HEADER } from './swr'; const STATIC_DATE = new Date('2000-01-01'); const fetchMock = jest.fn(); const cachesMock = { match: jest.fn(), put: jest.fn(), }; // @ts-ignore global.caches = { default: cachesMock }; global.fetch = fetchMock; beforeEach(() => { mockDate.set(STATIC_DATE); cachesMock.match.mockReset(); cachesMock.put.mockReset(); global.fetch = fetchMock.mockReset(); }); describe('swr', () => { describe('s-maxage=60, stale-while-revalidate', () => { describe('first request', () => { let cachesPutCall: [Request, Response]; let response; let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValue( new Response('', { headers: { 'cache-control': 's-maxage=60, stale-while-revalidate', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); cachesPutCall = cachesMock.put.mock.calls[0]; }); it('calls fetch as expected', () => { const [request] = fetchMock.mock.calls[0]; expect(fetchMock).toHaveBeenCalledTimes(1); expect(request.url).toBe( `https://example.com/?t=${STATIC_DATE.getTime()}` ); }); it('has the expected cache-control header', () => { expect(response.headers.get(CACHE_CONTROL_HEADER)).toBe( 'public, max-age=0, must-revalidate' ); }); it('caches the response', () => { const [request, response] = cachesMock.put.mock.calls[0]; expect(cachesMock.put).toHaveBeenCalledTimes(1); expect(request.url).toBe('https://example.com/'); // caches forever until revalidate expect(response.headers.get('cache-control')).toBe('immutable'); }); describe('… then second request (immediate)', () => { let response: Response; beforeEach(async () => { request = new Request('https://example.com'); cachesMock.match.mockResolvedValueOnce(cachesPutCall[1]); response = ((await swr({ request, event: mockFetchEvent(), })) as unknown) as Response; }); it('returns the expected cached response', () => { expect(response.headers.get('x-edge-cache-status')).toBe('HIT'); }); it('has the expected cache-control header', () => { expect(response.headers.get('cache-control')).toBe( 'public, max-age=0, must-revalidate' ); }); }); describe('… then second request (+7 days)', () => { let response: Response; beforeEach(async () => { request = new Request('https://example.com'); // mock clock forward 7 days mockDate.set( new Date(STATIC_DATE.getTime() + 1000 * 60 * 60 * 24 * 7) ); cachesMock.match.mockResolvedValueOnce(cachesPutCall[1]); response = ((await swr({ request, event: mockFetchEvent(), })) as unknown) as Response; }); it('returns the expected cached response', () => { expect(response.headers.get('x-edge-cache-status')).toBe( 'REVALIDATING' ); }); }); }); }); describe('max-age=10, s-maxage=60, stale-while-revalidate=60', () => { let cachesPutCall: [Request, Response]; let request; let response; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 'max-age=10, s-maxage=60, stale-while-revalidate=60', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); cachesPutCall = cachesMock.put.mock.calls[0]; }); it('calls fetch as expected', () => { const [request] = fetchMock.mock.calls[0]; expect(fetchMock).toHaveBeenCalledTimes(1); expect(request.url).toBe( `https://example.com/?t=${STATIC_DATE.getTime()}` ); }); it('has the expected response', () => { expect(response.headers.get('cache-control')).toBe('max-age=10'); }); it('caches the response', () => { const [request, response] = cachesPutCall; expect(cachesMock.put).toHaveBeenCalledTimes(1); expect(request.url).toBe('https://example.com/'); // stores the cached response for the additional swr window expect(response.headers.get('cache-control')).toBe('max-age=120'); }); describe('… then second request', () => { let res: Response; beforeEach(async () => { request = new Request('https://example.com'); cachesMock.match.mockResolvedValueOnce(cachesPutCall[1]); res = ((await swr({ request, event: mockFetchEvent(), })) as unknown) as Response; }); it('returns the expected cached response', () => { expect(res.headers.get('x-edge-cache-status')).toBe('HIT'); }); it('has the expected cache-control header', () => { expect(res.headers.get('Cache-Control')).toBe('max-age=10'); }); describe('… then + 61s', () => { let response: Response; let revalidateFetchDeferred; beforeEach(async () => { // move clock forward 61 seconds mockDate.set(new Date(STATIC_DATE.getTime() + 61 * 1000)); request = new Request('https://example.com'); // reset mock state fetchMock.mockReset(); cachesMock.put.mockReset(); cachesMock.match.mockResolvedValueOnce(cachesPutCall[1]); revalidateFetchDeferred = deferred(); // mock the revalidated request fetchMock.mockImplementationOnce(() => { revalidateFetchDeferred.resolve( new Response('', { headers: { 'cache-control': 's-maxage=60, stale-while-revalidate=60', }, }) ); return revalidateFetchDeferred.promise; }); response = ((await swr({ request, event: mockFetchEvent(), })) as unknown) as Response; }); it('returns the STALE response', () => { expect(response.headers.get('x-edge-cache-status')).toBe( 'REVALIDATING' ); }); it('updates the cache entry state to REVALIDATING', () => { const [, response] = cachesMock.put.mock.calls[0]; expect(response.headers.get('x-edge-cache-status')).toBe( 'REVALIDATING' ); }); it('fetches to revalidate', () => { expect(fetchMock).toHaveBeenCalled(); }); it('updates the cache with the fresh response', async () => { await revalidateFetchDeferred.promise; const [, response] = cachesMock.put.mock.calls[1]; expect(response.headers.get('x-edge-cache-status')).toBe('HIT'); }); }); }); }); describe('max-age=60', () => { let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 'max-age=60', }, }) ); await swr({ request, event: mockFetchEvent(), }); }); it('calls fetch as expected', () => { const [request] = fetchMock.mock.calls[0]; expect(fetchMock).toHaveBeenCalledTimes(1); expect(request.url).toBe( `https://example.com/?t=${STATIC_DATE.getTime()}` ); }); it('does NOT cache the response', () => { expect(cachesMock.put).not.toHaveBeenCalled(); }); }); describe('s-maxage=60', () => { let response; let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 's-maxage=60', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); }); it('caches the response', () => { expect(cachesMock.put).toHaveBeenCalled(); }); it('has the expected response', () => { expect(response.headers.get(CACHE_CONTROL_HEADER)).toBe( 'public, max-age=0, must-revalidate' ); }); }); describe('no-store, no-cache, max-age=0', () => { let response; let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 'no-store, no-cache, max-age=0', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); }); it('does NOT cache the response', () => { expect(cachesMock.put).not.toHaveBeenCalled(); }); it('has the expected response', () => { expect(response.headers.get(CACHE_CONTROL_HEADER)).toBe( 'public, max-age=0, must-revalidate' ); }); }); describe('404', () => { let response; let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('error', { status: 404, headers: { 'cache-control': 's-maxage=100', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); }); it('does NOT cache the response', () => { expect(cachesMock.put).not.toHaveBeenCalled(); }); }); describe('POST', () => { let response; let request; beforeEach(async () => { request = new Request('https://example.com', { method: 'POST', }); fetchMock.mockResolvedValueOnce( new Response('error', { headers: { 'cache-control': 's-maxage=100', }, }) ); response = await swr({ request, event: mockFetchEvent(), }); }); it('does NOT cache the response', () => { expect(cachesMock.put).not.toHaveBeenCalled(); }); }); describe('max-age=60', () => { let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 'max-age=60', }, }) ); await swr({ request, event: mockFetchEvent(), }); }); it('calls fetch as expected', () => { const [request] = fetchMock.mock.calls[0]; expect(fetchMock).toHaveBeenCalledTimes(1); expect(request.url).toBe(`https://example.com/?t=${Date.now()}`); }); it('does NOT cache the response', () => { expect(cachesMock.put).not.toHaveBeenCalled(); }); }); }); describe('s-maxage=1800, stale-while-revalidate=86400', () => { let request; beforeEach(async () => { request = new Request('https://example.com'); fetchMock.mockResolvedValueOnce( new Response('', { headers: { 'cache-control': 's-maxage=1800, stale-while-revalidate=86400', }, }) ); await swr({ request, event: mockFetchEvent(), }); }); // it('calls fetch as expected', () => { // const [request] = fetchMock.mock.calls[0]; // expect(fetchMock).toHaveBeenCalledTimes(1); // expect(request.url).toBe(`https://example.com/?t=${Date.now()}`); // }); it('does cache the response', () => { expect(cachesMock.put).toHaveBeenCalled(); }); }); const mockFetchEvent = () => (({ waitUntil: jest.fn(), } as unknown) as FetchEvent); const deferred = () => { let resolve; let reject; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; reject = _reject; }); return { promise, resolve, reject, }; };