Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 195a900

Browse files
committed
Add tests for Workers Sites
Cherrypicked from #656
1 parent b2a8295 commit 195a900

File tree

6 files changed

+326
-0
lines changed

6 files changed

+326
-0
lines changed

package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/miniflare/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"zod": "^3.20.6"
4545
},
4646
"devDependencies": {
47+
"@cloudflare/kv-asset-handler": "^0.3.0",
4748
"@cloudflare/workers-types": "^4.20230807.0",
4849
"@types/better-sqlite3": "^7.6.2",
4950
"@types/debug": "^4.1.7",
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "__STATIC_CONTENT_MANIFEST" {
2+
const value: string;
3+
export default value;
4+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
2+
import manifestJSON from "__STATIC_CONTENT_MANIFEST";
3+
const manifest = JSON.parse(manifestJSON);
4+
5+
export default <ExportedHandler<{ __STATIC_CONTENT: KVNamespace }>>{
6+
async fetch(request, env, ctx) {
7+
return await getAssetFromKV(
8+
{
9+
request,
10+
waitUntil(promise) {
11+
return ctx.waitUntil(promise);
12+
},
13+
},
14+
{
15+
ASSET_NAMESPACE: env.__STATIC_CONTENT,
16+
ASSET_MANIFEST: manifest,
17+
}
18+
);
19+
},
20+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getAssetFromKV } from "@cloudflare/kv-asset-handler";
2+
3+
addEventListener("fetch", (e) => {
4+
e.respondWith(
5+
getAssetFromKV(e).catch(
6+
(err) => new Response(err.stack, { status: err.status ?? 500 })
7+
)
8+
);
9+
});
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import assert from "assert";
2+
import fs from "fs/promises";
3+
import path from "path";
4+
import anyTest, { Macro, TestFn } from "ava";
5+
import esbuild from "esbuild";
6+
import { Miniflare } from "miniflare";
7+
import { useTmp } from "../../test-shared";
8+
9+
const FIXTURES_PATH = path.resolve(
10+
__dirname,
11+
"..",
12+
"..",
13+
"..",
14+
"..",
15+
"test",
16+
"fixtures",
17+
"sites"
18+
);
19+
const SERVICE_WORKER_ENTRY_PATH = path.join(FIXTURES_PATH, "service-worker.ts");
20+
const MODULES_ENTRY_PATH = path.join(FIXTURES_PATH, "modules.ts");
21+
22+
interface Context {
23+
serviceWorkerPath: string;
24+
modulesPath: string;
25+
}
26+
27+
const test = anyTest as TestFn<Context>;
28+
29+
test.before(async (t) => {
30+
// Build fixtures
31+
const tmp = await useTmp(t);
32+
await esbuild.build({
33+
entryPoints: [SERVICE_WORKER_ENTRY_PATH, MODULES_ENTRY_PATH],
34+
format: "esm",
35+
external: ["__STATIC_CONTENT_MANIFEST"],
36+
bundle: true,
37+
sourcemap: true,
38+
outdir: tmp,
39+
});
40+
t.context.serviceWorkerPath = path.join(tmp, "service-worker.js");
41+
t.context.modulesPath = path.join(tmp, "modules.js");
42+
});
43+
44+
type Route = keyof typeof routeContents;
45+
const routeContents = {
46+
"/": "<p>Index</p>",
47+
"/a.txt": "a",
48+
"/b/b.txt": "b",
49+
};
50+
51+
const getMacro: Macro<
52+
[{ siteInclude?: string[]; siteExclude?: string[] }, Set<Route>],
53+
Context
54+
> = {
55+
async exec(t, options, expectedRoutes) {
56+
const tmp = await useTmp(t);
57+
for (const [route, contents] of Object.entries(routeContents)) {
58+
const routePath = path.join(tmp, route === "/" ? "index.html" : route);
59+
await fs.mkdir(path.dirname(routePath), { recursive: true });
60+
await fs.writeFile(routePath, contents, "utf8");
61+
}
62+
63+
const mf = new Miniflare({
64+
...options,
65+
scriptPath: t.context.serviceWorkerPath,
66+
sitePath: tmp,
67+
});
68+
t.teardown(() => mf.dispose());
69+
70+
for (const [route, expectedContents] of Object.entries(routeContents)) {
71+
const res = await mf.dispatchFetch(`http://localhost:8787${route}`);
72+
const expected = expectedRoutes.has(route as Route);
73+
const text = (await res.text()).trim();
74+
t.is(res.status, expected ? 200 : 404, `${route}: ${text}`);
75+
if (expected) t.is(text, expectedContents, route);
76+
}
77+
},
78+
};
79+
80+
test(
81+
"gets all assets with no filter",
82+
getMacro,
83+
{},
84+
new Set<Route>(["/", "/a.txt", "/b/b.txt"])
85+
);
86+
test(
87+
"gets included assets with include filter",
88+
getMacro,
89+
{ siteInclude: ["b"] },
90+
new Set<Route>(["/b/b.txt"])
91+
);
92+
test(
93+
"gets all but excluded assets with include filter",
94+
getMacro,
95+
{ siteExclude: ["b"] },
96+
new Set<Route>(["/", "/a.txt"])
97+
);
98+
test(
99+
"gets included assets with include and exclude filters",
100+
getMacro,
101+
{ siteInclude: ["*.txt"], siteExclude: ["b"] },
102+
new Set<Route>(["/a.txt"])
103+
);
104+
105+
// Tests for checking different types of globs are matched correctly
106+
const matchMacro: Macro<[string], Context> = {
107+
async exec(t, include) {
108+
const tmp = await useTmp(t);
109+
const dir = path.join(tmp, "a", "b", "c");
110+
await fs.mkdir(dir, { recursive: true });
111+
await fs.writeFile(path.join(dir, "test.txt"), "test", "utf8");
112+
const mf = new Miniflare({
113+
siteInclude: [include],
114+
scriptPath: t.context.serviceWorkerPath,
115+
sitePath: tmp,
116+
});
117+
t.teardown(() => mf.dispose());
118+
const res = await mf.dispatchFetch("http://localhost:8787/a/b/c/test.txt");
119+
t.is(res.status, 200);
120+
await res.arrayBuffer();
121+
},
122+
};
123+
124+
test("matches file name pattern", matchMacro, "test.txt");
125+
test("matches exact pattern", matchMacro, "a/b/c/test.txt");
126+
test("matches extension patterns", matchMacro, "*.txt");
127+
test("matches globstar patterns", matchMacro, "**/*.txt");
128+
test("matches wildcard directory patterns", matchMacro, "a/*/c/*.txt");
129+
130+
test("doesn't cache assets", async (t) => {
131+
const tmp = await useTmp(t);
132+
const testPath = path.join(tmp, "test.txt");
133+
await fs.writeFile(testPath, "1", "utf8");
134+
135+
const mf = new Miniflare({
136+
scriptPath: t.context.serviceWorkerPath,
137+
sitePath: tmp,
138+
});
139+
t.teardown(() => mf.dispose());
140+
141+
const res1 = await mf.dispatchFetch("http://localhost:8787/test.txt");
142+
t.is(res1.headers.get("CF-Cache-Status"), "MISS");
143+
t.is(await res1.text(), "1");
144+
145+
await fs.writeFile(testPath, "2", "utf8");
146+
const res2 = await mf.dispatchFetch("http://localhost:8787/test.txt");
147+
t.is(res2.headers.get("CF-Cache-Status"), "MISS");
148+
t.is(await res2.text(), "2");
149+
});
150+
151+
test("gets assets with module worker", async (t) => {
152+
const tmp = await useTmp(t);
153+
const testPath = path.join(tmp, "test.txt");
154+
const dirPath = path.join(tmp, "dir");
155+
const nestedPath = path.join(dirPath, "nested.txt");
156+
await fs.writeFile(testPath, "test", "utf8");
157+
await fs.mkdir(dirPath, { recursive: true });
158+
await fs.writeFile(nestedPath, "nested", "utf8");
159+
160+
const mf = new Miniflare({
161+
modules: [{ type: "ESModule", path: t.context.modulesPath }],
162+
sitePath: tmp,
163+
});
164+
t.teardown(() => mf.dispose());
165+
166+
let res = await mf.dispatchFetch("http://localhost:8787/test.txt");
167+
t.is(await res.text(), "test");
168+
res = await mf.dispatchFetch("http://localhost:8787/dir/nested.txt");
169+
t.is(await res.text(), "nested");
170+
});
171+
172+
test("gets assets with percent-encoded paths", async (t) => {
173+
// https://github.com/cloudflare/miniflare/issues/326
174+
const tmp = await useTmp(t);
175+
const testPath = path.join(tmp, "ń.txt");
176+
await fs.writeFile(testPath, "test", "utf8");
177+
const mf = new Miniflare({
178+
scriptPath: t.context.serviceWorkerPath,
179+
sitePath: tmp,
180+
});
181+
t.teardown(() => mf.dispose());
182+
const res = await mf.dispatchFetch("http://localhost:8787/ń.txt");
183+
t.is(await res.text(), "test");
184+
});
185+
186+
test("static content namespace supports listing keys", async (t) => {
187+
const tmp = await useTmp(t);
188+
await fs.mkdir(path.join(tmp, "a", "b", "c"), { recursive: true });
189+
await fs.writeFile(path.join(tmp, "1.txt"), "one");
190+
await fs.writeFile(path.join(tmp, "2.txt"), "two");
191+
await fs.writeFile(path.join(tmp, "a", "3.txt"), "three");
192+
await fs.writeFile(path.join(tmp, "a", "b", "4.txt"), "four");
193+
await fs.writeFile(path.join(tmp, "a", "b", "c", "5.txt"), "five");
194+
await fs.writeFile(path.join(tmp, "a", "b", "c", "6.txt"), "six");
195+
await fs.writeFile(path.join(tmp, "a", "b", "c", "7.txt"), "seven");
196+
const mf = new Miniflare({
197+
verbose: true,
198+
scriptPath: t.context.serviceWorkerPath,
199+
sitePath: tmp,
200+
siteExclude: ["**/5.txt"],
201+
});
202+
t.teardown(() => mf.dispose());
203+
204+
const kv = await mf.getKVNamespace("__STATIC_CONTENT");
205+
let result = await kv.list();
206+
t.deepEqual(result, {
207+
keys: [
208+
{ name: "$__MINIFLARE_SITES__$/1.txt" },
209+
{ name: "$__MINIFLARE_SITES__$/2.txt" },
210+
{ name: "$__MINIFLARE_SITES__$/a%2F3.txt" },
211+
{ name: "$__MINIFLARE_SITES__$/a%2Fb%2F4.txt" },
212+
{ name: "$__MINIFLARE_SITES__$/a%2Fb%2Fc%2F6.txt" },
213+
{ name: "$__MINIFLARE_SITES__$/a%2Fb%2Fc%2F7.txt" },
214+
],
215+
list_complete: true,
216+
cacheStatus: null,
217+
});
218+
219+
// Check with prefix, cursor and limit
220+
result = await kv.list({ prefix: "$__MINIFLARE_SITES__$/a%2F", limit: 1 });
221+
assert(!result.list_complete);
222+
t.deepEqual(result, {
223+
keys: [{ name: "$__MINIFLARE_SITES__$/a%2F3.txt" }],
224+
list_complete: false,
225+
cursor: "JF9fTUlOSUZMQVJFX1NJVEVTX18kL2ElMkYzLnR4dA==",
226+
cacheStatus: null,
227+
});
228+
229+
result = await kv.list({
230+
prefix: "$__MINIFLARE_SITES__$/a%2F",
231+
limit: 2,
232+
cursor: result.cursor,
233+
});
234+
assert(!result.list_complete);
235+
t.deepEqual(result, {
236+
keys: [
237+
{ name: "$__MINIFLARE_SITES__$/a%2Fb%2F4.txt" },
238+
{ name: "$__MINIFLARE_SITES__$/a%2Fb%2Fc%2F6.txt" },
239+
],
240+
list_complete: false,
241+
cursor: "JF9fTUlOSUZMQVJFX1NJVEVTX18kL2ElMkZiJTJGYyUyRjYudHh0",
242+
cacheStatus: null,
243+
});
244+
245+
result = await kv.list({
246+
prefix: "$__MINIFLARE_SITES__$/a%2F",
247+
cursor: result.cursor,
248+
});
249+
t.deepEqual(result, {
250+
keys: [{ name: "$__MINIFLARE_SITES__$/a%2Fb%2Fc%2F7.txt" }],
251+
list_complete: true,
252+
cacheStatus: null,
253+
});
254+
});

0 commit comments

Comments
 (0)