Skip to content

Commit cd8d6f8

Browse files
authored
Add unit tests for cache interceptor (#577)
1 parent a51432e commit cd8d6f8

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/* eslint-disable sonarjs/no-duplicate-string */
2+
import { cacheInterceptor } from "@opennextjs/aws/core/routing/cacheInterceptor.js";
3+
import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js";
4+
import { Queue } from "@opennextjs/aws/queue/types.js";
5+
import { InternalEvent } from "@opennextjs/aws/types/open-next.js";
6+
import { fromReadableStream } from "@opennextjs/aws/utils/stream.js";
7+
import { vi } from "vitest";
8+
9+
vi.mock("@opennextjs/aws/adapters/config/index.js", () => ({
10+
NextConfig: {},
11+
PrerenderManifest: {
12+
routes: {
13+
"/albums": {
14+
initialRevalidateSeconds: false,
15+
srcRoute: "/albums",
16+
dataRoute: "/albums.rsc",
17+
},
18+
"/revalidate": {
19+
initialRevalidateSeconds: 60,
20+
srcRoute: null,
21+
dataRoute: "/_next/data/abc/revalidate.json",
22+
},
23+
},
24+
dynamicRoutes: {},
25+
},
26+
}));
27+
28+
vi.mock("@opennextjs/aws/core/routing/i18n/index.js", () => ({
29+
localizePath: (event: InternalEvent) => event.rawPath,
30+
}));
31+
32+
type PartialEvent = Partial<
33+
Omit<InternalEvent, "body" | "rawPath" | "query">
34+
> & { body?: string };
35+
36+
function createEvent(event: PartialEvent): InternalEvent {
37+
const [rawPath, qs] = (event.url ?? "/").split("?", 2);
38+
return {
39+
type: "core",
40+
method: event.method ?? "GET",
41+
rawPath,
42+
url: event.url ?? "/",
43+
body: Buffer.from(event.body ?? ""),
44+
headers: event.headers ?? {},
45+
query: convertFromQueryString(qs ?? ""),
46+
cookies: event.cookies ?? {},
47+
remoteAddress: event.remoteAddress ?? "::1",
48+
};
49+
}
50+
51+
const incrementalCache = {
52+
name: "mock",
53+
get: vi.fn(),
54+
set: vi.fn(),
55+
delete: vi.fn(),
56+
};
57+
58+
const tagCache = {
59+
name: "mock",
60+
getByTag: vi.fn(),
61+
getByPath: vi.fn(),
62+
getLastModified: vi.fn(),
63+
writeTags: vi.fn(),
64+
};
65+
66+
const queue = {
67+
name: "mock",
68+
send: vi.fn(),
69+
};
70+
71+
globalThis.incrementalCache = incrementalCache;
72+
globalThis.tagCache = tagCache;
73+
74+
declare global {
75+
var queue: Queue;
76+
}
77+
globalThis.queue = queue;
78+
79+
beforeEach(() => {
80+
vi.useFakeTimers().setSystemTime("2024-01-02T00:00:00Z");
81+
vi.clearAllMocks();
82+
});
83+
84+
describe("cacheInterceptor", () => {
85+
it("should take no action when next-action header is present", async () => {
86+
const event = createEvent({
87+
headers: {
88+
"next-action": "something",
89+
},
90+
});
91+
const result = await cacheInterceptor(event);
92+
93+
expect(result).toEqual(event);
94+
});
95+
96+
it("should take no action when x-prerender-revalidate header is present", async () => {
97+
const event = createEvent({
98+
headers: {
99+
"x-prerender-revalidate": "1",
100+
},
101+
});
102+
const result = await cacheInterceptor(event);
103+
104+
expect(result).toEqual(event);
105+
});
106+
107+
it("should take no action when incremental cache throws", async () => {
108+
const event = createEvent({
109+
url: "/albums",
110+
});
111+
112+
incrementalCache.get.mockRejectedValueOnce(new Error("mock error"));
113+
const result = await cacheInterceptor(event);
114+
115+
expect(result).toEqual(event);
116+
});
117+
118+
it("should retrieve app router content from cache", async () => {
119+
const event = createEvent({
120+
url: "/albums",
121+
});
122+
incrementalCache.get.mockResolvedValueOnce({
123+
value: {
124+
type: "app",
125+
html: "Hello, world!",
126+
},
127+
});
128+
129+
const result = await cacheInterceptor(event);
130+
131+
const body = await fromReadableStream(result.body);
132+
expect(body).toEqual("Hello, world!");
133+
expect(result).toEqual(
134+
expect.objectContaining({
135+
type: "core",
136+
statusCode: 200,
137+
isBase64Encoded: false,
138+
headers: expect.objectContaining({
139+
"cache-control": "s-maxage=31536000, stale-while-revalidate=2592000",
140+
"content-type": "text/html; charset=utf-8",
141+
etag: expect.any(String),
142+
"x-opennext-cache": "HIT",
143+
}),
144+
}),
145+
);
146+
});
147+
148+
it("should take no action when tagCache lasModified is -1", async () => {
149+
const event = createEvent({
150+
url: "/albums",
151+
});
152+
incrementalCache.get.mockResolvedValueOnce({
153+
value: {
154+
type: "app",
155+
html: "Hello, world!",
156+
},
157+
});
158+
tagCache.getLastModified.mockResolvedValueOnce(-1);
159+
160+
const result = await cacheInterceptor(event);
161+
162+
expect(result).toEqual(event);
163+
});
164+
165+
it("should retrieve page router content from stale cache", async () => {
166+
const event = createEvent({
167+
url: "/revalidate",
168+
});
169+
incrementalCache.get.mockResolvedValueOnce({
170+
value: {
171+
type: "page",
172+
html: "Hello, world!",
173+
revalidate: 60,
174+
},
175+
lastModified: new Date("2024-01-01T23:58:00Z").getTime(),
176+
});
177+
178+
const result = await cacheInterceptor(event);
179+
180+
const body = await fromReadableStream(result.body);
181+
expect(body).toEqual("Hello, world!");
182+
expect(result).toEqual(
183+
expect.objectContaining({
184+
type: "core",
185+
statusCode: 200,
186+
isBase64Encoded: false,
187+
headers: expect.objectContaining({
188+
"cache-control": "s-maxage=1, stale-while-revalidate=2592000",
189+
"content-type": "text/html; charset=utf-8",
190+
etag: expect.any(String),
191+
"x-opennext-cache": "STALE",
192+
}),
193+
}),
194+
);
195+
});
196+
197+
it("should retrieve page router content from active cache", async () => {
198+
const event = createEvent({
199+
url: "/revalidate",
200+
});
201+
incrementalCache.get.mockResolvedValueOnce({
202+
value: {
203+
type: "page",
204+
html: "Hello, world!",
205+
revalidate: 60,
206+
},
207+
lastModified: new Date("2024-01-02T00:00:00Z").getTime(),
208+
});
209+
210+
const result = await cacheInterceptor(event);
211+
212+
const body = await fromReadableStream(result.body);
213+
expect(body).toEqual("Hello, world!");
214+
expect(result).toEqual(
215+
expect.objectContaining({
216+
type: "core",
217+
statusCode: 200,
218+
isBase64Encoded: false,
219+
headers: expect.objectContaining({
220+
"cache-control": "s-maxage=60, stale-while-revalidate=2592000",
221+
"content-type": "text/html; charset=utf-8",
222+
etag: expect.any(String),
223+
"x-opennext-cache": "HIT",
224+
}),
225+
}),
226+
);
227+
});
228+
229+
it("should retrieve redirect content from cache", async () => {
230+
const event = createEvent({
231+
url: "/albums",
232+
});
233+
incrementalCache.get.mockResolvedValueOnce({
234+
value: {
235+
type: "redirect",
236+
meta: {
237+
status: 302,
238+
},
239+
},
240+
});
241+
242+
const result = await cacheInterceptor(event);
243+
244+
expect(result).toEqual(
245+
expect.objectContaining({
246+
type: "core",
247+
statusCode: 302,
248+
isBase64Encoded: false,
249+
headers: expect.objectContaining({
250+
"cache-control": "s-maxage=31536000, stale-while-revalidate=2592000",
251+
etag: expect.any(String),
252+
"x-opennext-cache": "HIT",
253+
}),
254+
}),
255+
);
256+
});
257+
258+
it("should take no action when cache returns unrecoginsed type", async () => {
259+
const event = createEvent({
260+
url: "/albums",
261+
});
262+
incrementalCache.get.mockResolvedValueOnce({
263+
value: {
264+
type: "?",
265+
html: "Hello, world!",
266+
},
267+
});
268+
269+
const result = await cacheInterceptor(event);
270+
271+
expect(result).toEqual(event);
272+
});
273+
});

0 commit comments

Comments
 (0)