1import * as AWS from "@/AWS";
2import { flociServices } from "@/AWS/Local/FlociServices.ts";
3import * as Test from "@/Test/Alchemy";
4import * as cloudfront from "@distilled.cloud/aws/cloudfront";
5import { describe, expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import * as Redacted from "effect/Redacted";
11import * as Schedule from "effect/Schedule";
12import * as HttpClient from "effect/http/HttpClient";
13import * as pathe from "pathe";
14import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
15
16// `dev: true` runs local providers behind the RPC sidecar proxy by default,
17// matching the process topology of the real `alchemy dev` command.
18//
19// These cases need floci's emulated CloudFront edge, which arrives with the
20// next `DEFAULT_FLOCI_IMAGE` pin bump. Until then, run them against a locally
21// built emulator: `ALCHEMY_FLOCI_IMAGE=floci:cf-edge pnpm test …`.
22const { test } = Test.make({ providers: AWS.providers(), dev: true });
23
24const fixtureDir = pathe.resolve(
25 import.meta.dirname,
26 "fixtures",
27 "staticsite-dev",
28);
29const viteFixtureDir = pathe.resolve(
30 import.meta.dirname,
31 "fixtures",
32 "vite-app",
33);
34// Clone under the alchemy package so `vite` resolves from the workspace's
35// hoisted node_modules (the fixture has no node_modules of its own).
36const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
37
38const htmlPage = (marker: string) => `<!doctype html>
39<html>
40 <head><title>${marker}</title></head>
41 <body><h1>${marker}</h1></body>
42</html>
43`;
44
45/**
46 * Fetch the Router the way a developer does: open its URL.
47 *
48 * No Host header, no emulator endpoint, no TLS exception — `router.url` under
49 * `alchemy dev` is a plain `http://localhost:<port>` the emulated distribution
50 * is served on, so a browser (and this test) can just GET it. Needing anything
51 * else here would mean the dev URL is not really usable.
52 */
53const fetchRouter = Effect.fn("fetchRouter")(function* (
54 routerUrl: string,
55 path: string,
56) {
57 const client = yield* HttpClient.HttpClient;
58 return yield* client
59 .get(`${routerUrl}${path}`)
60 .pipe(
61 Effect.retry({ schedule: Schedule.exponential("500 millis"), times: 6 }),
62 );
63});
64
65/**
66 * Build one Router-attached dev site: a cloned fixture whose long-lived dev
67 * server serves `site/<prefix>/index.html`, so the site behaves like a real
68 * static host mounted under the Router's path prefix.
69 */
70const makeSiteFixture = Effect.fn("makeSiteFixture")(function* (
71 prefix: string,
72 marker: string,
73) {
74 const fs = yield* FileSystem.FileSystem;
75 const path = yield* Path.Path;
76 const cwd = yield* cloneFixture(fixtureDir, {
77 prefix: `alchemy-aws-router-dev-${prefix}-`,
78 tempRoot,
79 entries: ["serve.mjs", "site"],
80 });
81 yield* fs.makeDirectory(path.join(cwd, "site", prefix), { recursive: true });
82 yield* fs.writeFileString(
83 path.join(cwd, "site", prefix, "index.html"),
84 htmlPage(marker),
85 );
86 return cwd;
87});
88
89/**
90 * What the Router answered with, when it isn't what the test expected.
91 *
92 * A typed failure (rather than an `expect` throw) so the assertion can be
93 * retried: the dev server behind the mount may still be warming up, and a
94 * live edit becomes visible one file-watcher tick after the write.
95 */
96class RouterBodyMismatch extends Data.TaggedError("RouterBodyMismatch")<{
97 url: string;
98 status: number;
99 problem: string;
100 bodyExcerpt: string;
101}> {}
102
103/**
104 * GET `path` through the Router and assert what came back — status, markers
105 * that must be present, and markers that must NOT be (the negative half is
106 * what pins longest-prefix routing: a body is only proof if it could have
107 * come from the *wrong* origin and didn't).
108 */
109const expectRouterBody = Effect.fn("expectRouterBody")(function* (
110 routerUrl: string,
111 path: string,
112 options: {
113 readonly includes?: ReadonlyArray<string>;
114 readonly excludes?: ReadonlyArray<string>;
115 readonly status?: number;
116 },
117) {
118 const client = yield* HttpClient.HttpClient;
119 const url = `${routerUrl}${path}`;
120 const status = options.status ?? 200;
121 return yield* Effect.gen(function* () {
122 const response = yield* client.get(url);
123 const body = yield* response.text;
124 const missing = (options.includes ?? []).filter((m) => !body.includes(m));
125 const present = (options.excludes ?? []).filter((m) => body.includes(m));
126 if (
127 response.status !== status ||
128 missing.length > 0 ||
129 present.length > 0
130 ) {
131 return yield* Effect.fail(
132 new RouterBodyMismatch({
133 url,
134 status: response.status,
135 problem: [
136 response.status !== status
137 ? `expected status ${status}`
138 : undefined,
139 missing.length > 0 ? `missing ${missing.join(", ")}` : undefined,
140 present.length > 0
141 ? `unexpectedly present ${present.join(", ")}`
142 : undefined,
143 ]
144 .filter(Boolean)
145 .join("; "),
146 bodyExcerpt: body.slice(0, 240),
147 }),
148 );
149 }
150 return body;
151 }).pipe(
152 Effect.retry({
153 schedule: Schedule.exponential("400 millis", 1.4),
154 times: 8,
155 }),
156 Effect.tapError((error) =>
157 Effect.logError(`expectRouterBody(${path}) failed`, error),
158 ),
159 );
160});
161
162/**
163 * Clone the plain-Vite fixture and stamp per-site markers into it, so a body
164 * served through the Router identifies WHICH dev server produced it.
165 *
166 * Returns the paths of the two files the live-edit assertions rewrite.
167 */
168const makeViteFixture = Effect.fn("makeViteFixture")(function* (
169 slug: string,
170 marker: string,
171) {
172 const fs = yield* FileSystem.FileSystem;
173 const path = yield* Path.Path;
174 const cwd = yield* cloneFixture(viteFixtureDir, {
175 prefix: `alchemy-aws-router-vite-${slug}-`,
176 tempRoot,
177 entries: [".gitignore", "package.json", "index.html", "src", "public"],
178 });
179 const indexPath = path.join(cwd, "index.html");
180 const mainPath = path.join(cwd, "src", "main.ts");
181 const index = yield* fs.readFileString(indexPath);
182 yield* fs.writeFileString(
183 indexPath,
184 index.replaceAll("VITE_AWS_PAGE_MARKER", `${marker}_PAGE`),
185 );
186 const main = yield* fs.readFileString(mainPath);
187 yield* fs.writeFileString(
188 mainPath,
189 main.replaceAll("VITE_AWS_MODULE_MARKER", `${marker}_MODULE`),
190 );
191 return { cwd, indexPath, mainPath };
192});
193
194describe(
195 "AWS.Website.Router local",
196 { tags: ["provider:aws", "provider:aws:website"] },
197 () => {
198 /**
199 * The whole point of the local Router: two sites, one Router, real HTTP
200 * through the emulated CloudFront distribution, each path prefix reaching
201 * its own dev server. No CloudFront deploy — the distribution, its KV
202 * store, its viewer-request function and the routing all run in floci.
203 */
204 test.provider(
205 "two dev sites route through one emulated distribution",
206 (stack) =>
207 Effect.gen(function* () {
208 yield* stack.destroy();
209
210 const cwdA = yield* makeSiteFixture("site-a", "router-dev-site-a");
211 const cwdB = yield* makeSiteFixture("site-b", "router-dev-site-b");
212
213 const deployed = yield* stack.deploy(
214 Effect.gen(function* () {
215 const router = yield* AWS.Website.Router("DevRouter", {});
216 const siteA = yield* AWS.Website.StaticSite("SiteA", {
217 path: cwdA,
218 dev: {
219 command: "bun serve.mjs",
220 env: { DEV_MARKER: "site-a" },
221 },
222 domain: { router, path: "/site-a" },
223 });
224 const siteB = yield* AWS.Website.StaticSite("SiteB", {
225 path: cwdB,
226 dev: {
227 command: "bun serve.mjs",
228 env: { DEV_MARKER: "site-b" },
229 },
230 domain: { router, path: "/site-b" },
231 });
232 return {
233 routerUrl: router.url,
234 distributionId: router.distributionId,
235 kvNamespace: router.kvNamespace,
236 siteA: { url: siteA.url, kvNamespace: siteA.kvNamespace },
237 siteB: { url: siteB.url, kvNamespace: siteB.kvNamespace },
238 };
239 }),
240 );
241
242 const routerUrl = deployed.routerUrl as string;
243 // The Router is a real (emulated) CloudFront distribution — the same
244 // resource graph a deploy produces, not a dev-only substitute — served
245 // on a local port of its own, because `E….cloudfront.net` resolves to
246 // nothing on a developer's machine.
247 expect(routerUrl).toMatch(/^http:\/\/localhost:\d+$/);
248 // Each site kept its own dev server as its `url` (HMR is the point of
249 // dev) while still registering with the Router.
250 expect(deployed.siteA.url).toMatch(/^http:\/\/localhost:\d+/);
251 expect(deployed.siteB.url).toMatch(/^http:\/\/localhost:\d+/);
252 expect(deployed.siteA.url).not.toBe(deployed.siteB.url);
253 expect(deployed.siteA.kvNamespace).toBeDefined();
254 expect(deployed.siteB.kvNamespace).not.toBe(
255 deployed.siteA.kvNamespace,
256 );
257
258 // ── Routing: each prefix reaches its own dev server ────────────────
259 const a = yield* fetchRouter(routerUrl, "/site-a/");
260 expect(a.status).toBe(200);
261 expect(yield* a.text).toContain("router-dev-site-a");
262
263 const b = yield* fetchRouter(routerUrl, "/site-b/");
264 expect(b.status).toBe(200);
265 expect(yield* b.text).toContain("router-dev-site-b");
266
267 // A mount is a path SEGMENT prefix, not a string prefix: `/site-a-x`
268 // shares seven characters with `/site-a` and must not be routed to it.
269 const neighbour = yield* fetchRouter(routerUrl, "/site-a-x");
270 expect(yield* neighbour.text).not.toContain("router-dev-site-a");
271
272 // ── The origin received the request CloudFront would have sent ─────
273 const echo = yield* fetchRouter(routerUrl, "/site-a/__echo");
274 expect(echo.status).toBe(200);
275 const echoed = (yield* echo.json) as {
276 marker: string;
277 path: string;
278 headers: Record<string, string>;
279 };
280 expect(echoed.marker).toBe("site-a");
281 expect(echoed.path).toBe("/site-a/__echo");
282 // `routeSite` sets x-forwarded-host to the viewer's Host before it
283 // rewrites the origin — the site's dev server sees the hostname the
284 // request arrived on, exactly as a deployed server origin would.
285 expect(echoed.headers["x-forwarded-host"]).toBe(
286 new URL(routerUrl).host,
287 );
288
289 // ── Live edit: the dev server reads from disk per request, so the
290 // next edge request serves the new content without re-applying ─────
291 const fs = yield* FileSystem.FileSystem;
292 const path = yield* Path.Path;
293 yield* fs.writeFileString(
294 path.join(cwdA, "site", "site-a", "index.html"),
295 htmlPage("router-dev-site-a-v2"),
296 );
297 const edited = yield* fetchRouter(routerUrl, "/site-a/");
298 expect(yield* edited.text).toContain("router-dev-site-a-v2");
299
300 yield* stack.destroy();
301 }),
302 { tags: ["local"], timeout: 300_000 },
303 );
304
305 /**
306 * The framework path — the one a real `alchemy dev` user is on.
307 *
308 * A framework dev server is not a hand-written fixture: it binds where the
309 * framework decides (Vite picks `[::1]`), serves modules the HTML it
310 * generated points at, and rewrites those URLs by `base`. The emulated edge
311 * runs in a container and reaches the host through its gateway, so a
312 * loopback-only listener answers nothing and every request through the
313 * Router 502s.
314 *
315 * So this case is deliberately end-to-end through `router.url` only: the
316 * HTML shell, the module URLs that HTML actually references, and a live
317 * edit landing on a later request. Nothing here inspects the bind address
318 * (not portable) — the route working IS the proof.
319 */
320 test.provider(
321 "a Vite dev server serves its whole mount through the Router",
322 (stack) =>
323 Effect.gen(function* () {
324 const fs = yield* FileSystem.FileSystem;
325
326 yield* stack.destroy();
327
328 const root = yield* makeViteFixture("root", "ROUTER_VITE_ROOT");
329 const app = yield* makeViteFixture("app", "ROUTER_VITE_APP");
330
331 const deployed = yield* stack.deploy(
332 Effect.gen(function* () {
333 const router = yield* AWS.Website.Router("ViteRouter", {});
334 const rootSite = yield* AWS.Website.Vite("RootSite", {
335 rootDir: root.cwd,
336 domain: { router },
337 });
338 const appSite = yield* AWS.Website.Vite("AppSite", {
339 rootDir: app.cwd,
340 domain: { router, path: "/app" },
341 // The Router forwards the FULL uri to the origin — it never
342 // strips the mount prefix — so the dev server has to serve at
343 // the prefix. `vite.base` is what makes Vite do that, for both
344 // the module URLs it writes into the HTML and the requests it
345 // then has to answer. Passed as the deploy-time override bag
346 // (merged over the fixture's config file, which has none).
347 vite: { base: "/app/" },
348 });
349 return {
350 routerUrl: router.url,
351 rootUrl: rootSite.url,
352 appUrl: appSite.url,
353 };
354 }),
355 );
356
357 const routerUrl = deployed.routerUrl as string;
358 expect(routerUrl).toMatch(/^http:\/\/localhost:\d+$/);
359 // Each site's own `url` is still its dev server (HMR is the point).
360 expect(deployed.rootUrl).toMatch(/^http:\/\/localhost:\d+/);
361 expect(deployed.appUrl).toMatch(/^http:\/\/localhost:\d+/);
362 expect(deployed.appUrl).not.toBe(deployed.rootUrl);
363
364 // ── The HTML shell at each mount, from its own dev server ──────────
365 const rootHtml = yield* expectRouterBody(routerUrl, "/", {
366 includes: ["ROUTER_VITE_ROOT_PAGE"],
367 excludes: ["ROUTER_VITE_APP_PAGE"],
368 });
369 const appHtml = yield* expectRouterBody(routerUrl, "/app/", {
370 includes: ["ROUTER_VITE_APP_PAGE"],
371 excludes: ["ROUTER_VITE_ROOT_PAGE"],
372 });
373
374 // Vite rewrote the asset URLs by `base`, so the browser's next hops
375 // are the prefixed ones. Assert that BEFORE fetching them — otherwise
376 // the fetches below could pass against URLs no real browser requests.
377 expect(appHtml).toContain('src="/app/src/main.ts"');
378 expect(appHtml).toContain("/app/@vite/client");
379 expect(rootHtml).toContain('src="/src/main.ts"');
380 expect(rootHtml).toContain("/@vite/client");
381
382 // ── The assets that HTML points at, through the Router ─────────────
383 // A mount only works if its sub-resources resolve; an index page that
384 // renders while its modules 404 is a broken site.
385 yield* expectRouterBody(routerUrl, "/app/src/main.ts", {
386 includes: ["ROUTER_VITE_APP_MODULE"],
387 excludes: ["ROUTER_VITE_ROOT_MODULE"],
388 });
389 yield* expectRouterBody(routerUrl, "/app/@vite/client", {
390 includes: ["export"],
391 });
392 yield* expectRouterBody(routerUrl, "/src/main.ts", {
393 includes: ["ROUTER_VITE_ROOT_MODULE"],
394 excludes: ["ROUTER_VITE_APP_MODULE"],
395 });
396 yield* expectRouterBody(routerUrl, "/@vite/client", {
397 includes: ["export"],
398 });
399
400 // ── Longest-prefix routing: neither mount answers the other's ──────
401 // The root site matches `/` and would happily serve `/app/...` if the
402 // longer prefix didn't win; the app site must not leak out of `/app`.
403 yield* expectRouterBody(routerUrl, "/app/src/main.ts", {
404 excludes: ["ROUTER_VITE_ROOT_MODULE"],
405 });
406 // `/app-other` shares a string prefix with `/app` but is NOT under the
407 // mount — it belongs to the root site (whose SPA fallback answers it).
408 yield* expectRouterBody(routerUrl, "/app-other", {
409 includes: ["ROUTER_VITE_ROOT_PAGE"],
410 excludes: ["ROUTER_VITE_APP_PAGE"],
411 });
412
413 // ── Live edit: the origin is a RUNNING dev server, not a copy ──────
414 // Nothing is re-applied; the next request through the edge has to
415 // show the new bytes. `?v=2` also defeats any cache in between.
416 const editedMain = (yield* fs.readFileString(app.mainPath)).replace(
417 "ROUTER_VITE_APP_MODULE",
418 "ROUTER_VITE_APP_MODULE_V2",
419 );
420 yield* fs.writeFileString(app.mainPath, editedMain);
421 yield* expectRouterBody(routerUrl, "/app/src/main.ts?v=2", {
422 includes: ["ROUTER_VITE_APP_MODULE_V2"],
423 });
424
425 const editedIndex = (yield* fs.readFileString(
426 root.indexPath,
427 )).replace("ROUTER_VITE_ROOT_PAGE", "ROUTER_VITE_ROOT_PAGE_V2");
428 yield* fs.writeFileString(root.indexPath, editedIndex);
429 yield* expectRouterBody(routerUrl, "/?v=2", {
430 includes: ["ROUTER_VITE_ROOT_PAGE_V2"],
431 });
432
433 yield* stack.destroy();
434 }),
435 // 300s like the sibling cases: fixture clone + `bun install` + vite dev
436 // boot through the emulated edge stack up under whole-suite concurrency
437 // (the test itself runs in ~6s in isolation).
438 { tags: ["local"], timeout: 300_000 },
439 );
440
441 /**
442 * The emulated runtime must be at least as restrictive as CloudFront's.
443 * CloudFront Functions are not Node: there is no `fetch`. Code that reaches
444 * for one has to fail locally, or `alchemy dev` becomes a way to ship
445 * broken edge code.
446 */
447 test.provider(
448 "edge code that reaches outside the CloudFront runtime fails locally",
449 (stack) =>
450 Effect.gen(function* () {
451 yield* stack.destroy();
452
453 const deployed = yield* stack.deploy(
454 Effect.gen(function* () {
455 const router = yield* AWS.Website.Router("SandboxRouter", {
456 edge: {
457 viewerRequest: {
458 injection: `await fetch("https://example.com/");`,
459 },
460 },
461 });
462 return { routerUrl: router.url };
463 }),
464 );
465
466 const response = yield* fetchRouter(
467 deployed.routerUrl as string,
468 "/anything",
469 );
470 expect(response.status).toBe(502);
471 const body = yield* response.text;
472 expect(body).toContain("fetch is not defined");
473 expect(body).toContain("not Node.js");
474
475 yield* stack.destroy();
476 }),
477 { tags: ["local"], timeout: 180_000 },
478 );
479
480 /**
481 * `TestFunction` and the edge are backed by the same runtime, so the request
482 * object the API reports is the request the origin actually received. That
483 * is what makes a local result comparable to the same call against real AWS
484 * (which runs the function in AWS's own engine against the DEVELOPMENT
485 * stage — seconds, no distribution deploy).
486 */
487 test.provider(
488 "TestFunction agrees with the edge",
489 (stack) =>
490 Effect.gen(function* () {
491 yield* stack.destroy();
492
493 const cwd = yield* makeSiteFixture("docs", "router-parity-docs");
494
495 const deployed = yield* stack.deploy(
496 Effect.gen(function* () {
497 const router = yield* AWS.Website.Router("ParityRouter", {});
498 yield* AWS.Website.StaticSite("Docs", {
499 path: cwd,
500 dev: { command: "bun serve.mjs", env: { DEV_MARKER: "docs" } },
501 domain: { router, path: "/docs" },
502 });
503 return { routerUrl: router.url };
504 }),
505 );
506
507 const routerUrl = deployed.routerUrl as string;
508 const host = new URL(routerUrl).host;
509
510 // What the edge produced, as observed by the origin itself.
511 const echo = yield* fetchRouter(routerUrl, "/docs/__echo?q=1");
512 const echoed = (yield* echo.json) as {
513 path: string;
514 headers: Record<string, string>;
515 };
516
517 // The same event, through the TestFunction API. The out-of-band SDK
518 // calls are pinned to the emulator explicitly — the test process's
519 // own distilled clients otherwise point at the real account.
520 const result = yield* Effect.gen(function* () {
521 const functions = yield* cloudfront.listFunctions({});
522 const summary = functions.FunctionList?.Items?.find(
523 (item) =>
524 item.FunctionConfig.Comment === "ParityRouter viewer request",
525 );
526 expect(summary).toBeDefined();
527 const described = yield* cloudfront.describeFunction({
528 Name: summary!.Name,
529 Stage: "DEVELOPMENT",
530 });
531 return yield* cloudfront.testFunction({
532 Name: summary!.Name,
533 IfMatch: described.ETag!,
534 Stage: "DEVELOPMENT",
535 EventObject: new TextEncoder().encode(
536 JSON.stringify({
537 version: "1.0",
538 context: { eventType: "viewer-request" },
539 viewer: { ip: "127.0.0.1" },
540 request: {
541 method: "GET",
542 uri: "/docs/__echo",
543 querystring: { q: { value: "1" } },
544 headers: { host: { value: host } },
545 cookies: {},
546 },
547 }),
548 ),
549 });
550 }).pipe(Effect.provide(flociServices()));
551
552 expect(result.TestResult?.FunctionErrorMessage).toBeUndefined();
553 // `FunctionOutput` is modelled as a sensitive string, so it arrives
554 // wrapped — unwrap before parsing.
555 const rawOutput = result.TestResult?.FunctionOutput;
556 const output = JSON.parse(
557 rawOutput === undefined
558 ? "{}"
559 : typeof rawOutput === "string"
560 ? rawOutput
561 : Redacted.value(rawOutput),
562 ) as { request?: { uri: string; headers: Record<string, any> } };
563 expect(output.request?.uri).toBe(echoed.path);
564 expect(output.request?.headers["x-forwarded-host"]?.value).toBe(
565 echoed.headers["x-forwarded-host"],
566 );
567
568 yield* stack.destroy();
569 }),
570 { tags: ["provider:aws:cloudfront", "local"], timeout: 300_000 },
571 );
572
573 /**
574 * The oracle for everything above: run the same function through AWS's own
575 * engine and compare. `TestFunction` executes against the `DEVELOPMENT`
576 * stage, so this costs seconds and never touches a distribution.
577 *
578 * Gated because it needs real credentials — `AWS_TEST_CLOUDFRONT_FUNCTION=1
579 * pnpm test test/AWS/Website/Router.local.test.ts --profile testing`.
580 */
581 test.provider.skipIf(process.env.AWS_TEST_CLOUDFRONT_FUNCTION !== "1")(
582 "the emulated runtime agrees with the real CloudFront runtime",
583 () =>
584 Effect.gen(function* () {
585 const name = "alchemy-cf-runtime-parity";
586 const event = new TextEncoder().encode(
587 JSON.stringify({
588 version: "1.0",
589 context: { eventType: "viewer-request" },
590 viewer: { ip: "127.0.0.1" },
591 request: {
592 method: "GET",
593 uri: "/docs",
594 querystring: { q: { value: "1" } },
595 headers: { host: { value: "example.cloudfront.net" } },
596 cookies: {},
597 },
598 }),
599 );
600
601 /** Create, test, and delete a function against whichever endpoint is in scope. */
602 const runThere = Effect.fn("runThere")(function* (
603 suffix: string,
604 code: string,
605 ) {
606 const created = yield* cloudfront.createFunction({
607 Name: `${name}-${suffix}`,
608 FunctionConfig: {
609 Comment: "parity",
610 Runtime: "cloudfront-js-2.0",
611 },
612 FunctionCode: new TextEncoder().encode(code),
613 });
614 const result = yield* cloudfront.testFunction({
615 Name: `${name}-${suffix}`,
616 IfMatch: created.ETag!,
617 Stage: "DEVELOPMENT",
618 EventObject: event,
619 });
620 yield* cloudfront
621 .deleteFunction({
622 Name: `${name}-${suffix}`,
623 IfMatch: created.ETag!,
624 })
625 .pipe(Effect.ignore);
626 const raw = result.TestResult?.FunctionOutput;
627 return {
628 error: result.TestResult?.FunctionErrorMessage,
629 output:
630 raw === undefined
631 ? undefined
632 : typeof raw === "string"
633 ? raw
634 : Redacted.value(raw),
635 };
636 });
637
638 // A well-behaved function: both engines must produce the same request.
639 const wellBehaved = `async function handler(event) {
640 event.request.headers["x-forwarded-host"] = event.request.headers.host;
641 event.request.uri = event.request.uri + "/index.html";
642 return event.request;
643}`;
644 const local = yield* runThere("ok", wellBehaved).pipe(
645 Effect.provide(flociServices()),
646 );
647 const real = yield* runThere("ok", wellBehaved);
648 expect(local.error).toBeUndefined();
649 expect(real.error).toBeUndefined();
650 expect(JSON.parse(local.output!)).toEqual(JSON.parse(real.output!));
651
652 // Reaching outside the runtime: both engines must report an error.
653 const escaping = `async function handler(event) {
654 await fetch("https://example.com/");
655 return event.request;
656}`;
657 const localError = yield* runThere("escape", escaping).pipe(
658 Effect.provide(flociServices()),
659 );
660 const realError = yield* runThere("escape", escaping);
661 expect(localError.error).toBeDefined();
662 expect(realError.error).toBeDefined();
663 }),
664 { tags: ["provider:aws:cloudfront", "live"], timeout: 180_000 },
665 );
666 },
667);