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 SECRET_A =
21 "kyTZf6QHTPVdpDjLWWbYO7DI3Z6f3wWvECDCtMHEOSomCnq0db4DBzowg4QH51jJZUw5n4nGYNGmkJhCfn+9Ag==";
22const SECRET_B =
23 "8nNZTjsM1Yu0aFYNYTcMM2KFCDc2cMKQz3mNkLV0XzUgDgQ4Mc7XhhSh0RUFhd6OYzz6Vl5hUTUVUYz1JBdt0g==";
24
25// Ride out fresh-token 403 blips on out-of-band calls.
26const retryForbidden = <A, E extends { _tag: string }, R>(
27 eff: Effect.Effect<A, E, R>,
28) =>
29 eff.pipe(
30 Effect.retry({
31 while: (e) => e._tag === "Forbidden",
32 schedule: Schedule.exponential("500 millis"),
33 times: 8,
34 }),
35 );
36
37const getTsig = (accountId: string, tsigId: string) =>
38 retryForbidden(dns.getZoneTransferTsig({ accountId, tsigId }));
39
40// A deleted TSIG surfaces as the typed `TsigNotFound` (HTTP 404).
41const expectGone = (accountId: string, tsigId: string) =>
42 getTsig(accountId, tsigId).pipe(
43 Effect.flatMap(() => Effect.fail({ _tag: "TsigNotDeleted" } as const)),
44 Effect.catchTag("TsigNotFound", () => Effect.void),
45 Effect.retry({
46 while: (e) => e._tag === "TsigNotDeleted",
47 schedule: Schedule.max([
48 Schedule.exponential("500 millis"),
49 Schedule.recurs(10),
50 ]),
51 }),
52 );
53
54test.provider(
55 "create, rotate the secret in place, and delete a TSIG",
56 (stack) =>
57 Effect.gen(function* () {
58 const { accountId } = yield* yield* CloudflareEnvironment;
59
60 yield* stack.destroy();
61
62 const created = yield* stack.deploy(
63 Cloudflare.DNS.ZoneTransferTsig("TestTsig", {
64 name: "alchemy-dnszt-tsig-test.",
65 algo: "hmac-sha512.",
66 secret: Redacted.make(SECRET_A),
67 }),
68 );
69 expect(created.tsigId).toBeDefined();
70 expect(created.accountId).toEqual(accountId);
71 expect(created.name).toEqual("alchemy-dnszt-tsig-test.");
72 expect(created.algo).toEqual("hmac-sha512.");
73 // The secret is never persisted in attributes.
74 expect("secret" in created).toBe(false);
75
76 // Out-of-band verify via the SDK.
77 const live = yield* getTsig(accountId, created.tsigId);
78 expect(live.name).toEqual("alchemy-dnszt-tsig-test.");
79 expect(live.secret).toEqual(SECRET_A);
80
81 // Rotate the secret in place — same physical TSIG.
82 const rotated = yield* stack.deploy(
83 Cloudflare.DNS.ZoneTransferTsig("TestTsig", {
84 name: "alchemy-dnszt-tsig-test.",
85 algo: "hmac-sha512.",
86 secret: Redacted.make(SECRET_B),
87 }),
88 );
89 expect(rotated.tsigId).toEqual(created.tsigId);
90
91 const liveRotated = yield* getTsig(accountId, created.tsigId);
92 expect(liveRotated.secret).toEqual(SECRET_B);
93
94 yield* stack.destroy();
95 yield* expectGone(accountId, created.tsigId);
96
97 // Re-running destroy is idempotent.
98 yield* stack.destroy();
99 }).pipe(logLevel),
100 {
101 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
102 timeout: 120_000,
103 },
104);
105
106// Canonical `list()` test (account-scoped collection): deploy a real TSIG,
107// resolve the provider from context via the typed `findProvider`, call
108// `list()`, and assert the deployed TSIG appears in the exhaustively-paginated
109// result.
110test.provider(
111 "list enumerates the deployed TSIG",
112 (stack) =>
113 Effect.gen(function* () {
114 yield* stack.destroy();
115
116 const deployed = yield* stack.deploy(
117 Cloudflare.DNS.ZoneTransferTsig("ListTsig", {
118 name: "alchemy-dnszt-tsig-list.",
119 algo: "hmac-sha512.",
120 secret: Redacted.make(SECRET_A),
121 }),
122 );
123
124 const provider = yield* Provider.findProvider(
125 Cloudflare.DNS.ZoneTransferTsig,
126 );
127 const all = yield* provider.list();
128
129 expect(all.some((t) => t.tsigId === deployed.tsigId)).toBe(true);
130
131 yield* stack.destroy();
132 }).pipe(logLevel),
133 {
134 tags: ["provider:cloudflare", "provider:cloudflare:dns", "live"],
135 timeout: 120_000,
136 },
137);