1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as cloudfront from "@distilled.cloud/aws/cloudfront";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import { fileURLToPath } from "node:url";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Anchor the fixture to the repo root regardless of the runner's cwd.
12const fixtureDir = fileURLToPath(
13 new URL("../../../../../examples/aws-static-site/site", import.meta.url),
14);
15
16// Gated: CloudFront Distribution create blocks on Status === "Deployed"
17// (~5-15 min) and destroy requires disable -> wait -> delete (another
18// ~5-15 min), so the full Router lifecycle exceeds any sane test budget.
19// Skipped under --fast (FAST=1) (same gate as the
20// AWS.CloudFront suites).
21const runLive = !process.env.FAST;
22
23describe.skipIf(!runLive)(
24 "AWS.Website.Router",
25 {
26 tags: [
27 "provider:aws",
28 "provider:aws:cloudfront",
29 "provider:aws:website",
30 "live",
31 ],
32 },
33 () => {
34 test.provider(
35 "create router with static-site attached via KV routing",
36 (stack) =>
37 Effect.gen(function* () {
38 yield* stack.destroy();
39
40 const deployed = yield* stack.deploy(
41 Effect.gen(function* () {
42 const router = yield* AWS.Website.Router("Router", {
43 invalidation: {
44 paths: "all",
45 wait: true,
46 },
47 });
48
49 const site = yield* AWS.Website.StaticSite("DocsSite", {
50 path: fixtureDir,
51 forceDestroy: true,
52 domain: {
53 router,
54 },
55 });
56
57 return {
58 site,
59 router,
60 };
61 }),
62 );
63
64 expect(deployed.router.distribution.distributionId).toBeDefined();
65 expect(deployed.router.kvStoreArn).toBeDefined();
66
67 // urls contract (cloudfront-default arm): a router without a
68 // domain serves only at the distribution's own URL (its CloudFront
69 // default domain live, a local edge port under the emulator), and
70 // `url` is always `urls[0]`.
71 expect(deployed.router.urls).toEqual([
72 deployed.router.distribution.url,
73 ]);
74 expect(deployed.router.url).toBe(deployed.router.urls[0]);
75 // A path-only attached site inherits the router's primary URL.
76 expect(deployed.site.urls).toEqual([deployed.router.url]);
77 expect(deployed.site.url).toBe(deployed.site.urls[0]);
78
79 const config = yield* cloudfront.getDistributionConfig({
80 Id: deployed.router.distribution.distributionId,
81 });
82 expect(
83 config.DistributionConfig?.DefaultCacheBehavior
84 ?.FunctionAssociations?.Quantity,
85 ).toBeGreaterThanOrEqual(1);
86
87 yield* stack.destroy();
88 yield* assertDistributionDeleted(
89 deployed.router.distribution.distributionId,
90 );
91 }),
92 // Create waits for Status === "Deployed" (~5 min) and destroy is
93 // disable -> wait -> delete (~5-15 min more): 600s was measured too
94 // small — the run died mid-destroy with green assertions.
95 // CloudFront full lifecycle (create + KV-routed assertions + disable +
96 // delete) measures ~6m with bounded polls; generous headroom for
97 // propagation variance. If this ever times out mid-destroy again,
98 // suspect a hung poll first (see the Effect.timeout guards in
99 // Distribution.ts), not CloudFront.
100 { timeout: 1_500_000 },
101 );
102 },
103);
104
105const assertDistributionDeleted = (distributionId: string) =>
106 cloudfront.getDistribution({ Id: distributionId }).pipe(
107 Effect.flatMap(() => Effect.fail(new Error("DistributionStillExists"))),
108 Effect.catchTag("NoSuchDistribution", () => Effect.void),
109 Effect.retry({
110 while: (error) =>
111 error instanceof Error && error.message === "DistributionStillExists",
112 schedule: Schedule.max([
113 Schedule.fixed("10 seconds"),
114 Schedule.recurs(60),
115 ]),
116 }),
117 );