1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as dns from "@distilled.cloud/cloudflare/dns";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Redacted from "effect/Redacted";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// Deterministic key material (this is test-only, not a real secret).
20const TSIG_SECRET =
21 "kyTZf6QHTPVdpDjLWWbYO7DI3Z6f3wWvECDCtMHEOSomCnq0db4DBzowg4QH51jJZUw5n4nGYNGmkJhCfn+9Ag==";
22
23// Ride out fresh-token 403 blips on out-of-band calls.
24const retryForbidden = <A, E extends { _tag: string }, R>(
25 eff: Effect.Effect<A, E, R>,
26) =>
27 eff.pipe(
28 Effect.retry({
29 while: (e) => e._tag === "Forbidden",
30 schedule: Schedule.exponential("500 millis"),
31 times: 8,
32 }),
33 );
34
35const getPeer = (accountId: string, peerId: string) =>
36 retryForbidden(dns.getZoneTransferPeer({ accountId, peerId }));
37
38// A deleted peer surfaces as the typed `PeerNotFound` (HTTP 404).
39const expectGone = (accountId: string, peerId: string) =>
40 getPeer(accountId, peerId).pipe(
41 Effect.flatMap(() => Effect.fail({ _tag: "PeerNotDeleted" } as const)),
42 Effect.catchTag("PeerNotFound", () => Effect.void),
43 Effect.retry({
44 while: (e) => e._tag === "PeerNotDeleted",
45 schedule: Schedule.max([
46 Schedule.exponential("500 millis"),
47 Schedule.recurs(10),
48 ]),
49 }),
50 );
51
52test.provider(
53 "create with connection settings, update in place, and delete a peer",
54 (stack) =>
55 Effect.gen(function* () {
56 const { accountId } = yield* yield* CloudflareEnvironment;
57
58 yield* stack.destroy();
59
60 // Create applies name-only POST followed by a PUT with the
61 // connection settings.
62 const created = yield* stack.deploy(
63 Cloudflare.DNS.ZoneTransferPeer("TestPeer", {
64 name: "alchemy-dnszt-peer-test",
65 ip: "192.0.2.53",
66 port: 53,
67 }),
68 );
69 expect(created.peerId).toBeDefined();
70 expect(created.accountId).toEqual(accountId);
71 expect(created.name).toEqual("alchemy-dnszt-peer-test");
72 expect(created.ip).toEqual("192.0.2.53");
73 expect(created.port).toEqual(53);
74
75 // Out-of-band verify via the SDK.
76 const live = yield* getPeer(accountId, created.peerId);
77 expect(live.name).toEqual("alchemy-dnszt-peer-test");
78 expect(live.ip).toEqual("192.0.2.53");
79 expect(live.port).toEqual(53);
80
81 // Update the connection settings in place — same physical peer.
82 const updated = yield* stack.deploy(
83 Cloudflare.DNS.ZoneTransferPeer("TestPeer", {
84 name: "alchemy-dnszt-peer-test-renamed",
85 ip: "198.51.100.53",
86 port: 5353,
87 }),
88 );
89 expect(updated.peerId).toEqual(created.peerId);
90 expect(updated.name).toEqual("alchemy-dnszt-peer-test-renamed");
91 expect(updated.ip).toEqual("198.51.100.53");
92 expect(updated.port).toEqual(5353);
93
94 const liveUpdated = yield* getPeer(accountId, created.peerId);
95 expect(liveUpdated.ip).toEqual("198.51.100.53");
96 expect(liveUpdated.port).toEqual(5353);
97
98 yield* stack.destroy();
99 yield* expectGone(accountId, created.peerId);
100
101 // Re-running destroy is idempotent.
102 yield* stack.destroy();
103 }).pipe(logLevel),
104 {
105 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
106 timeout: 120_000,
107 },
108);
109
110test.provider(
111 "wires a TSIG reference through to the peer",
112 (stack) =>
113 Effect.gen(function* () {
114 const { accountId } = yield* yield* CloudflareEnvironment;
115
116 yield* stack.destroy();
117
118 const { peer, tsig } = yield* stack.deploy(
119 Effect.gen(function* () {
120 const tsig = yield* Cloudflare.DNS.ZoneTransferTsig("PeerTsig", {
121 name: "alchemy-dnszt-peer-tsig-test.",
122 algo: "hmac-sha512.",
123 secret: Redacted.make(TSIG_SECRET),
124 });
125 const peer = yield* Cloudflare.DNS.ZoneTransferPeer("TsigPeer", {
126 name: "alchemy-dnszt-peer-tsig-peer",
127 ip: "192.0.2.54",
128 tsigId: tsig.tsigId,
129 });
130 return { peer, tsig };
131 }),
132 );
133 expect(peer.tsigId).toEqual(tsig.tsigId);
134
135 const live = yield* getPeer(accountId, peer.peerId);
136 expect(live.tsigId).toEqual(tsig.tsigId);
137
138 yield* stack.destroy();
139 yield* expectGone(accountId, peer.peerId);
140 }).pipe(logLevel),
141 {
142 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
143 timeout: 120_000,
144 },
145);
146
147test.provider(
148 "list enumerates the deployed peer",
149 (stack) =>
150 Effect.gen(function* () {
151 yield* stack.destroy();
152
153 const deployed = yield* stack.deploy(
154 Cloudflare.DNS.ZoneTransferPeer("ListPeer", {
155 name: "alchemy-dnszt-peer-list",
156 ip: "192.0.2.55",
157 port: 53,
158 }),
159 );
160
161 const provider = yield* Provider.findProvider(
162 Cloudflare.DNS.ZoneTransferPeer,
163 );
164 const all = yield* provider.list();
165
166 expect(all.some((p) => p.peerId === deployed.peerId)).toBe(true);
167
168 yield* stack.destroy();
169 }).pipe(logLevel),
170 {
171 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
172 timeout: 120_000,
173 },
174);