1import * as zoneRrsets from "@distilled.cloud/hetzner/zone_rrsets";
2import { Action } from "@/Action";
3import * as Hetzner from "@/Hetzner";
4import * as Test from "@/Test/Alchemy";
5import * as zones from "@distilled.cloud/hetzner/zones";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Hetzner.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
19
20const waitUntilGone = (zoneId: number, name: string, type: string) =>
21 zoneRrsets
22 .getZoneRrset({
23 id_or_name: String(zoneId),
24 rr_name: name,
25 rr_type: type,
26 })
27 .pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("1 second"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37const waitUntilZoneGone = (zoneId: number) =>
38 zones.getZone({ id_or_name: String(zoneId) }).pipe(
39 Effect.as("found" as const),
40 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
41 Effect.repeat({
42 schedule: Schedule.spaced("1 second"),
43 until: (status) => status === "gone",
44 times: 10,
45 }),
46 );
47
48test.provider.skipIf(!hasHetznerCreds)(
49 "ReadWriteDns: get, list, create, update and delete a record set",
50 (stack) =>
51 Effect.gen(function* () {
52 yield* stack.destroy();
53
54 const out = yield* stack.deploy(
55 Effect.gen(function* () {
56 const zone = yield* Hetzner.Zone("DnsBindingZone", {
57 ttl: 3600,
58 });
59 const recordSet = yield* Hetzner.RecordSet("Www", {
60 zone,
61 name: "www",
62 type: "A",
63 records: [{ value: "192.0.2.1" }],
64 ttl: 300,
65 });
66
67 const Probe = Action(
68 "Probe",
69 Effect.gen(function* () {
70 // Capture the record-set Output so Probe waits for Www.
71 yield* recordSet.id;
72 const dns = yield* Hetzner.ReadWriteDns(zone);
73 return Effect.fn(function* () {
74 const listed = yield* dns.listRecordSets({
75 name: "www",
76 type: ["A"],
77 });
78 const got = yield* dns.getRecordSet("www", "A");
79
80 const created = yield* dns.createRecordSet({
81 name: "api",
82 type: "A",
83 records: [{ value: "192.0.2.50" }],
84 ttl: 300,
85 });
86 yield* Hetzner.waitForZoneAction(created.action);
87
88 const set = yield* dns.setRecordSetRecords("api", "A", {
89 records: [{ value: "192.0.2.51" }],
90 });
91 yield* Hetzner.waitForZoneAction(set.action);
92 const updated = yield* dns.getRecordSet("api", "A");
93
94 const deleted = yield* dns.deleteRecordSet("api", "A");
95 yield* Hetzner.waitForZoneAction(deleted.action);
96
97 return {
98 listedCount: listed.rrsets.length,
99 listedValue: listed.rrsets[0]?.records[0]?.value,
100 gotId: got.rrset.id,
101 gotValue: got.rrset.records[0]?.value,
102 createdId: created.rrset.id,
103 updatedValue: updated.rrset.records[0]?.value,
104 };
105 });
106 }),
107 );
108
109 return {
110 zone,
111 recordSet,
112 probe: yield* Probe({}),
113 };
114 }),
115 );
116
117 expect(out.recordSet.id).toEqual("www/A");
118 expect(out.probe.gotId).toEqual("www/A");
119 expect(out.probe.gotValue).toEqual("192.0.2.1");
120 expect(out.probe.listedCount).toBeGreaterThan(0);
121 expect(out.probe.listedValue).toEqual("192.0.2.1");
122 expect(out.probe.createdId).toEqual("api/A");
123 expect(out.probe.updatedValue).toEqual("192.0.2.51");
124
125 const fetched = yield* zoneRrsets.getZoneRrset({
126 id_or_name: String(out.zone.zoneId),
127 rr_name: "www",
128 rr_type: "A",
129 });
130 expect(fetched.rrset.id).toEqual("www/A");
131 expect(fetched.rrset.records.map((record) => record.value)).toEqual([
132 "192.0.2.1",
133 ]);
134
135 const apiGone = yield* zoneRrsets
136 .getZoneRrset({
137 id_or_name: String(out.zone.zoneId),
138 rr_name: "api",
139 rr_type: "A",
140 })
141 .pipe(
142 Effect.as("found" as const),
143 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
144 );
145 expect(apiGone).toEqual("gone");
146
147 yield* stack.destroy();
148
149 const gone = yield* waitUntilGone(out.zone.zoneId, "www", "A");
150 expect(gone).toEqual("gone");
151 const zoneGone = yield* waitUntilZoneGone(out.zone.zoneId);
152 expect(zoneGone).toEqual("gone");
153 }).pipe(logLevel),
154 {
155 tags: [
156 "provider:hetzner",
157 "provider:hetzner:recordset",
158 "provider:hetzner:service",
159 "provider:hetzner:zone",
160 "live",
161 ],
162 timeout: 120_000,
163 },
164);