Skip to content
Source
Sign inSign up
Open History to see the last commit

CustomDomain.test.ts

171 lines6.6 KB

1import * as Cloudflare from "@alchemy.run/cloudflare";
2import { CloudflareEnvironment } from "@alchemy.run/cloudflare";
3import { findZoneByName } from "@alchemy.run/cloudflare/Zone";
4import { CustomDomain } from "@alchemy.run/neon/CustomDomain";
5import { Function } from "@alchemy.run/neon/Function";
6import { Project } from "@alchemy.run/neon/Project";
7import { providers } from "@alchemy.run/neon/Providers";
8import { expect } from "@alchemy.run/test";
9import * as Test from "@alchemy.run/test/Alchemy";
10import * as dns from "@distilled.cloud/cloudflare/dns";
11import * as Api from "@distilled.cloud/neon";
12import * as AlchemyOutput from "alchemy/Output";
13import * as Config from "effect/Config";
14import * as Effect from "effect/Effect";
15import * as HttpClient from "effect/http/HttpClient";
16import * as Layer from "effect/Layer";
17import * as Schedule from "effect/Schedule";
18import * as Stream from "effect/Stream";
19​
20const { test } = Test.make({
21 providers: Layer.mergeAll(providers(), Cloudflare.providers()),
22});
23test.provider(
24 "custom domain registers independently and returns DNS before activation",
25 (stack) =>
26 Effect.gen(function* () {
27 yield* stack.destroy();
28 const tls = yield* Config.String("NODE_TLS_REJECT_UNAUTHORIZED").pipe(
29 Config.withDefault("1"),
30 );
31 expect(tls).not.toBe("0");
32 const { accountId } = yield* yield* CloudflareEnvironment;
33 const zone = yield* findZoneByName({
34 accountId,
35 name: "alchemy-test-2.us",
36 });
37 if (!zone) {
38 return yield* Effect.fail(new Error("alchemy-test-2.us not found"));
39 }
40 const zoneId = zone.id;
41 const resources = Effect.gen(function* () {
42 const project = yield* Project("DomainProject", {
43 region: "aws-us-east-2",
44 });
45 const api = yield* Function("Api", {
46 project,
47 main: new URL("./fixtures/function-bare.ts", import.meta.url).href,
48 });
49 return { project, api };
50 });
51 const deployed = yield* stack.deploy(
52 Effect.gen(function* () {
53 const { project, api } = yield* resources;
54 const domain = yield* CustomDomain("Domain", {
55 function: api,
56 hostname: api.slug.pipe(AlchemyOutput.map((slug) => `${slug}.alchemy-test-2.us`)),
57 });
58 const record = yield* Cloudflare.DNS.Record("DomainCname", {
59 zoneId,
60 name: domain.hostname,
61 type: "CNAME",
62 content: domain.cnameTarget,
63 proxied: false,
64 ttl: 60,
65 });
66 return { project, api, domain, record };
67 }),
68 );
69 expect(deployed.domain.cnameTarget.length).toBeGreaterThan(0);
70 expect(deployed.domain.url).toBe(`https://${deployed.domain.hostname}`);
71 const liveRecord = yield* dns.getRecord({
72 zoneId,
73 dnsRecordId: deployed.record.recordId,
74 });
75 expect(liveRecord.name).toBe(deployed.domain.hostname);
76 expect(liveRecord.type).toBe("CNAME");
77 expect(liveRecord.content).toBe(deployed.domain.cnameTarget);
78 expect(liveRecord.proxied).toBe(false);
79 expect(liveRecord.ttl).toBe(60);
80 const observeDomain = Api.listProjectBranchCustomDomains({
81 project_id: deployed.api.projectId,
82 branch_id: deployed.api.branchId,
83 }).pipe(
84 Effect.map(({ custom_domains }) =>
85 custom_domains.find((domain) => domain.domain === deployed.domain.hostname),
86 ),
87 );
88 const registered = yield* observeDomain;
89 expect(registered?.entity_type).toBe("function");
90 expect(registered?.entity_id).toBe(deployed.api.slug);
91 expect(registered?.cname_target).toBe(deployed.domain.cnameTarget);
92 const client = yield* HttpClient.HttpClient;
93 const activation = yield* Effect.gen(function* () {
94 const domain = yield* observeDomain;
95 const https = yield* Effect.gen(function* () {
96 if (domain?.dns_status !== "ok") {
97 return { status: 0, body: "DNS validation pending" };
98 }
99 const response = yield* client.get(deployed.domain.url);
100 const body = yield* response.text;
101 return { status: response.status, body };
102 }).pipe(
103 Effect.timeout("6 seconds"),
104 Effect.catch((error) =>
105 Effect.logWarning("Custom domain HTTPS probe failed", error).pipe(
106 Effect.as({ status: 0, body: String(error) }),
107 ),
108 ),
109 );
110 yield* Effect.logInfo("Custom domain activation", { domain, https });
111 return { domain, https };
112 }).pipe(
113 Effect.repeat({
114 // Neon's background DNS check lands around 45-50s, so leave headroom.
115 schedule: Schedule.spaced("5 seconds"),
116 times: 23,
117 until: ({ domain, https }) =>
118 domain?.status === "active" &&
119 domain.dns_status === "ok" &&
120 domain.binding_status === "present" &&
121 https.status === 200 &&
122 https.body === "bare-v2",
123 }),
124 Effect.timeout("150 seconds"),
125 );
126 expect(activation.domain?.status).toBe("active");
127 expect(activation.domain?.dns_status).toBe("ok");
128 expect(activation.domain?.binding_status).toBe("present");
129 expect(activation.https.status).toBe(200);
130 expect(activation.https.body).toBe("bare-v2");
131​
132 const retained = yield* stack.deploy(resources);
133 expect(retained.project.projectId).toBe(deployed.project.projectId);
134 expect(retained.api.slug).toBe(deployed.api.slug);
135 const remainingRecords = yield* dns.listRecords
136 .items({
137 zoneId,
138 name: { exact: deployed.domain.hostname },
139 type: "CNAME",
140 })
141 .pipe(Stream.runCollect);
142 expect(remainingRecords.length).toBe(0);
143 expect(yield* observeDomain).toBeUndefined();
144 const retainedProject = yield* Api.getProject({
145 project_id: deployed.project.projectId,
146 });
147 expect(retainedProject.project.id).toBe(deployed.project.projectId);
148 yield* stack.destroy();
149 const projectExists = yield* Api.getProject({
150 project_id: deployed.project.projectId,
151 }).pipe(
152 Effect.as(true),
153 Effect.catchTag("NotFound", () => Effect.succeed(false)),
154 );
155 expect(projectExists).toBe(false);
156 yield* Effect.logInfo("Custom domain, DNS record, and project independently absent");
157 }),
158 {
159 tags: [
160 "provider:cloudflare",
161 "provider:cloudflare:dns",
162 "provider:cloudflare:zone",
163 "provider:neon",
164 "provider:neon:customdomain",
165 "provider:neon:function",
166 "provider:neon:project",
167 "live",
168 ],
169 timeout: 240_000,
170 },
171);