Skip to content

Commit 03f168d

Browse files
committed
test: add test case with multiple fetches for same resource and test asserting we only check blobs once per request for same key
1 parent 8675b3e commit 03f168d

File tree

2 files changed

+140
-1
lines changed

2 files changed

+140
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const API_BASE = process.env.API_BASE || 'https://api.tvmaze.com/shows/'
2+
3+
async function doFetch(params) {
4+
const res = await fetch(new URL(params.id, API_BASE).href, {
5+
next: { revalidate: false },
6+
})
7+
return res.json()
8+
}
9+
10+
async function getData(params) {
11+
// trigger fetches in parallel to ensure we only do one cache lookup for ongoing fetches
12+
const [data1, data2] = await Promise.all([doFetch(params), doFetch(params)])
13+
// also trigger fetch after to make sure we reuse already fetched cache
14+
const data3 = await doFetch(params)
15+
16+
if (data1?.name !== data2?.name || data1?.name !== data3?.name) {
17+
throw new Error(
18+
`Should have 3 names that are the same, instead got [${data1?.name}, ${data2?.name}, ${data3?.name}]`,
19+
)
20+
}
21+
22+
return data1
23+
}
24+
25+
export default async function Page({ params }) {
26+
const data = await getData(params)
27+
28+
return (
29+
<>
30+
<h1>Using same fetch multiple times</h1>
31+
<dl>
32+
<dt>Show</dt>
33+
<dd data-testid="name">{data.name}</dd>
34+
<dt>Param</dt>
35+
<dd data-testid="id">{params.id}</dd>
36+
<dt>Time</dt>
37+
<dd data-testid="date-now">{Date.now()}</dd>
38+
<dt>Time from fetch response</dt>
39+
<dd data-testid="date-from-response">{data.date ?? 'no-date-in-response'}</dd>
40+
</dl>
41+
</>
42+
)
43+
}
44+
45+
// make page dynamic, but still use fetch cache
46+
export const fetchCache = 'force-cache'
47+
export const dynamic = 'force-dynamic'

tests/integration/fetch-handler.test.ts

+93-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,33 @@ import { v4 } from 'uuid'
66
import { afterAll, beforeAll, beforeEach, expect, test, vi } from 'vitest'
77
import { type FixtureTestContext } from '../utils/contexts.js'
88
import { createFixture, invokeFunction, runPlugin, runPluginStep } from '../utils/fixture.js'
9-
import { generateRandomObjectID, startMockBlobStore } from '../utils/helpers.js'
9+
import { generateRandomObjectID, getBlobServerGets, startMockBlobStore } from '../utils/helpers.js'
1010
import { nextVersionSatisfies } from '../utils/next-version-helpers.mjs'
1111

12+
function isFetch(key: string) {
13+
// exclude tag manifests (starting with `_N_T_`), pages (starting with `/`) and static html files (keys including `.html`)
14+
return !key.startsWith('_N_T_') && !key.startsWith('/') && !key.includes('.html')
15+
}
16+
17+
expect.extend({
18+
toBeDistinct(received: string[]) {
19+
const { isNot } = this
20+
const pass = new Set(received).size === received.length
21+
return {
22+
pass,
23+
message: () => `${received} is${isNot ? ' not' : ''} array with distinct values`,
24+
}
25+
},
26+
})
27+
28+
interface CustomMatchers<R = unknown> {
29+
toBeDistinct(): R
30+
}
31+
32+
declare module 'vitest' {
33+
interface Assertion<T = any> extends CustomMatchers<T> {}
34+
}
35+
1236
// Disable the verbose logging of the lambda-local runtime
1337
getLogger().level = 'alert'
1438

@@ -304,3 +328,71 @@ test<FixtureTestContext>('if the fetch call is cached correctly (cached page res
304328
}),
305329
)
306330
})
331+
332+
test<FixtureTestContext>('does not fetch same cached fetch data from blobs twice for the same request', async (ctx) => {
333+
await createFixture('revalidate-fetch', ctx)
334+
await runPluginStep(ctx, 'onPreBuild')
335+
await runPlugin(ctx)
336+
337+
handlerCalled = 0
338+
const request1 = await invokeFunction(ctx, {
339+
url: 'same-fetch-multiple-times/99',
340+
})
341+
342+
const request1FetchDate = load(request1.body)('[data-testid="date-from-response"]').text()
343+
const request1Name = load(request1.body)('[data-testid="name"]').text()
344+
345+
expect(request1.statusCode, 'Tested page should work').toBe(200)
346+
expect(request1Name, 'Test setup should use test API mock').toBe('Fake response')
347+
348+
expect(
349+
handlerCalled,
350+
'Cache should be empty, and we should hit mock endpoint once to warm up the cache',
351+
).toBe(1)
352+
353+
const request1FetchCacheKeys = getBlobServerGets(ctx, isFetch)
354+
355+
expect(
356+
request1FetchCacheKeys.length,
357+
'tested page should be doing 3 fetch calls to render single page - we should only try to get cached fetch data from blobs once',
358+
).toBe(1)
359+
360+
const request1AllCacheKeys = getBlobServerGets(ctx)
361+
expect(
362+
request1AllCacheKeys,
363+
'expected blobs for all types of values to be retrieved at most once per key (including fetch data, tag manifests, static files)',
364+
).toBeDistinct()
365+
366+
ctx.blobServerGetSpy.mockClear()
367+
handlerCalled = 0
368+
const request2 = await invokeFunction(ctx, {
369+
url: 'same-fetch-multiple-times/99',
370+
})
371+
372+
const request2FetchDate = load(request2.body)('[data-testid="date-from-response"]').text()
373+
const request2Name = load(request2.body)('[data-testid="name"]').text()
374+
375+
expect(request2.statusCode, 'Tested page should work').toBe(200)
376+
expect(request2Name, 'Test setup should use test API mock').toBe('Fake response')
377+
expect(request2FetchDate, 'Cached fetch data should be used for second request').toBe(
378+
request1FetchDate,
379+
)
380+
381+
expect(handlerCalled, 'Cache should be warm, and we should not hit mock endpoint').toBe(0)
382+
383+
const request2FetchCacheKeys = getBlobServerGets(ctx, isFetch)
384+
385+
expect(
386+
request2FetchCacheKeys.length,
387+
'We should not reuse in-memory cache from first request and have one fetch blob call for second request',
388+
).toBe(1)
389+
expect(request2FetchCacheKeys, 'Same fetch keys should be used in both requests').toEqual(
390+
request1FetchCacheKeys,
391+
)
392+
393+
const request2AllCacheKeys = getBlobServerGets(ctx)
394+
expect(
395+
request2AllCacheKeys,
396+
'expected blobs for all types of values to be retrieved at most once per key (including fetch data, tag manifests, static files)',
397+
).toBeDistinct()
398+
})

0 commit comments

Comments
 (0)