puls-dev 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +11 -11
  2. package/dist/bin/install-shell.js +5 -6
  3. package/dist/bin/puls.js +10 -3
  4. package/dist/core/config.d.ts +4 -0
  5. package/dist/core/decorators.d.ts +4 -0
  6. package/dist/core/decorators.js +2 -0
  7. package/dist/core/parallel.test.js +4 -3
  8. package/dist/core/resource.d.ts +2 -1
  9. package/dist/core/resource.js +4 -2
  10. package/dist/core/stack.d.ts +4 -0
  11. package/dist/core/stack.js +8 -8
  12. package/dist/providers/aws/acm.test.d.ts +1 -0
  13. package/dist/providers/aws/acm.test.js +167 -0
  14. package/dist/providers/aws/cloudfront.test.d.ts +1 -0
  15. package/dist/providers/aws/cloudfront.test.js +170 -0
  16. package/dist/providers/aws/fargate.test.d.ts +1 -0
  17. package/dist/providers/aws/fargate.test.js +244 -0
  18. package/dist/providers/aws/rds.test.d.ts +1 -0
  19. package/dist/providers/aws/rds.test.js +219 -0
  20. package/dist/providers/aws/sqs.test.d.ts +1 -0
  21. package/dist/providers/aws/sqs.test.js +181 -0
  22. package/dist/providers/cloudflare/api.d.ts +15 -0
  23. package/dist/providers/cloudflare/api.js +199 -0
  24. package/dist/providers/cloudflare/index.d.ts +14 -0
  25. package/dist/providers/cloudflare/index.js +19 -0
  26. package/dist/providers/cloudflare/kv.d.ts +20 -0
  27. package/dist/providers/cloudflare/kv.js +69 -0
  28. package/dist/providers/cloudflare/kv.test.d.ts +1 -0
  29. package/dist/providers/cloudflare/kv.test.js +134 -0
  30. package/dist/providers/cloudflare/r2.d.ts +14 -0
  31. package/dist/providers/cloudflare/r2.js +57 -0
  32. package/dist/providers/cloudflare/r2.test.d.ts +1 -0
  33. package/dist/providers/cloudflare/r2.test.js +132 -0
  34. package/dist/providers/cloudflare/worker.d.ts +28 -0
  35. package/dist/providers/cloudflare/worker.js +172 -0
  36. package/dist/providers/cloudflare/worker.test.d.ts +1 -0
  37. package/dist/providers/cloudflare/worker.test.js +220 -0
  38. package/dist/providers/cloudflare/zone.d.ts +42 -0
  39. package/dist/providers/cloudflare/zone.js +280 -0
  40. package/dist/providers/cloudflare/zone.test.d.ts +1 -0
  41. package/dist/providers/cloudflare/zone.test.js +284 -0
  42. package/dist/providers/firebase/auth.test.d.ts +1 -0
  43. package/dist/providers/firebase/auth.test.js +145 -0
  44. package/dist/providers/firebase/hosting.test.js +7 -6
  45. package/dist/providers/firebase/storage.test.d.ts +1 -0
  46. package/dist/providers/firebase/storage.test.js +148 -0
  47. package/package.json +6 -2
@@ -0,0 +1,280 @@
1
+ import { BaseBuilder } from "../../core/resource.js";
2
+ import { Output } from "../../core/output.js";
3
+ import { getCloudflareApi, getCloudflareAccountId } from "./api.js";
4
+ import { loadRecordsFromFile } from "../../core/parser.js";
5
+ function getFullRecordName(name, zone) {
6
+ const cleanName = name.trim();
7
+ if (cleanName === "" || cleanName === "@")
8
+ return zone;
9
+ if (cleanName.endsWith(zone))
10
+ return cleanName;
11
+ return `${cleanName}.${zone}`;
12
+ }
13
+ export class ZoneBuilder extends BaseBuilder {
14
+ domainName;
15
+ out = {
16
+ id: new Output(),
17
+ };
18
+ resolvedId = null;
19
+ records = [];
20
+ constructor(domainName) {
21
+ super(domainName);
22
+ this.domainName = domainName;
23
+ this.discoveryPromise = this.discoverZone(domainName);
24
+ }
25
+ async discoverZone(name) {
26
+ try {
27
+ const api = getCloudflareApi();
28
+ const res = await api.get(`/zones?name=${name}`);
29
+ return (res.result ?? []).find((z) => z.name === name) ?? null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ record(nameOrPath, type, value, ttl, priority, port, weight, flags, tag, proxied) {
36
+ if (arguments.length === 1 &&
37
+ typeof nameOrPath === "string" &&
38
+ (nameOrPath.endsWith(".yaml") || nameOrPath.endsWith(".yml") || nameOrPath.endsWith(".json"))) {
39
+ const loaded = loadRecordsFromFile(nameOrPath);
40
+ for (const r of loaded) {
41
+ this.records.push({
42
+ name: r.name,
43
+ type: r.type,
44
+ value: r.value,
45
+ ttl: r.ttl,
46
+ priority: r.priority,
47
+ port: r.port,
48
+ weight: r.weight,
49
+ flags: r.flags,
50
+ tag: r.tag,
51
+ proxied: r.proxied,
52
+ });
53
+ }
54
+ return this;
55
+ }
56
+ this.records.push({
57
+ name: nameOrPath,
58
+ type: type,
59
+ value: value,
60
+ ttl,
61
+ priority,
62
+ port,
63
+ weight,
64
+ flags,
65
+ tag,
66
+ proxied,
67
+ });
68
+ return this;
69
+ }
70
+ pointer(name, target, proxied) {
71
+ this.records.push({ type: "A", name, value: target, proxied });
72
+ return this;
73
+ }
74
+ cname(name, target, proxied) {
75
+ this.records.push({ type: "CNAME", name, value: target, proxied });
76
+ return this;
77
+ }
78
+ aaaa(name, target, proxied) {
79
+ this.records.push({ type: "AAAA", name, value: target, proxied });
80
+ return this;
81
+ }
82
+ txt(name, target) {
83
+ this.records.push({ type: "TXT", name, value: target });
84
+ return this;
85
+ }
86
+ mx(name, target, priority = 10) {
87
+ this.records.push({ type: "MX", name, value: target, priority });
88
+ return this;
89
+ }
90
+ srv(name, target, port, priority = 10, weight = 10) {
91
+ this.records.push({ type: "SRV", name, value: target, port, priority, weight });
92
+ return this;
93
+ }
94
+ caa(name, tag, target, flags = 0) {
95
+ this.records.push({ type: "CAA", name, value: target, tag, flags });
96
+ return this;
97
+ }
98
+ async deploy() {
99
+ const dryRun = this.isDryRunActive();
100
+ const existing = await this.discoveryPromise;
101
+ const api = getCloudflareApi();
102
+ console.log(`\n🌐 Finalizing DNS Zone for "${this.domainName}"...`);
103
+ if (existing) {
104
+ this.resolvedId = existing.id;
105
+ this.out.id.resolve(existing.id);
106
+ console.log(` ✅ Zone "${this.domainName}" exists (id=${existing.id})`);
107
+ }
108
+ else {
109
+ if (dryRun) {
110
+ console.log(` 📝 [PLAN] Create Cloudflare Zone "${this.domainName}"`);
111
+ this.resolvedId = "PENDING";
112
+ this.out.id.resolve("PENDING");
113
+ }
114
+ else {
115
+ const accountId = getCloudflareAccountId();
116
+ const res = await api.post("/zones", {
117
+ name: this.domainName,
118
+ account: { id: accountId },
119
+ type: "full",
120
+ });
121
+ this.resolvedId = res.result.id;
122
+ this.out.id.resolve(this.resolvedId);
123
+ console.log(`🚀 Created Cloudflare Zone "${this.domainName}" (id=${this.resolvedId})`);
124
+ }
125
+ }
126
+ let existingRecords = [];
127
+ if (existing && !dryRun) {
128
+ try {
129
+ const res = await api.get(`/zones/${this.resolvedId}/dns_records?per_page=100`);
130
+ existingRecords = res.result ?? [];
131
+ }
132
+ catch {
133
+ existingRecords = [];
134
+ }
135
+ }
136
+ const consumedRecordIds = new Set();
137
+ for (const record of this.records) {
138
+ let data;
139
+ if (record.value instanceof Output) {
140
+ data = await record.value.get();
141
+ }
142
+ else if (record.value && typeof record.value === "object" && "out" in record.value) {
143
+ const out = record.value.out;
144
+ if (out && out.ip instanceof Output) {
145
+ data = await out.ip.get();
146
+ }
147
+ else if (out && out.publicIp instanceof Output) {
148
+ data = await out.publicIp.get();
149
+ }
150
+ else {
151
+ data = String(record.value);
152
+ }
153
+ }
154
+ else if (record.value instanceof BaseBuilder) {
155
+ if (typeof record.value.getPublicIp === "function") {
156
+ data = await record.value.getPublicIp();
157
+ }
158
+ else {
159
+ data = String(record.value);
160
+ }
161
+ }
162
+ else {
163
+ data = String(record.value);
164
+ }
165
+ const targetFullName = getFullRecordName(record.name, this.domainName);
166
+ const targetTtl = record.ttl ?? 3600;
167
+ const targetProxied = !!record.proxied;
168
+ // Match logic helper
169
+ const isMatch = (r) => {
170
+ if (r.type !== record.type || r.name !== targetFullName)
171
+ return false;
172
+ if (record.type === "MX") {
173
+ return r.content === data && (r.priority ?? 10) === (record.priority ?? 10);
174
+ }
175
+ if (record.type === "SRV") {
176
+ return (r.data?.target === data &&
177
+ r.data?.port === (record.port ?? 5060) &&
178
+ r.data?.priority === (record.priority ?? 10) &&
179
+ r.data?.weight === (record.weight ?? 10));
180
+ }
181
+ if (record.type === "CAA") {
182
+ return (r.data?.value === data &&
183
+ r.data?.tag === (record.tag ?? "issue") &&
184
+ r.data?.flags === (record.flags ?? 0));
185
+ }
186
+ return r.content === data && !!r.proxied === targetProxied;
187
+ };
188
+ const perfectMatch = existingRecords.find((r) => !consumedRecordIds.has(r.id) && isMatch(r));
189
+ if (perfectMatch) {
190
+ consumedRecordIds.add(perfectMatch.id);
191
+ console.log(` ✅ ${record.type} ${targetFullName} is up to date (→ ${data})`);
192
+ continue;
193
+ }
194
+ const updateableMatch = existingRecords.find((r) => !consumedRecordIds.has(r.id) && r.type === record.type && r.name === targetFullName);
195
+ // Construct payload
196
+ const payload = {
197
+ type: record.type,
198
+ name: record.name,
199
+ ttl: targetTtl,
200
+ };
201
+ if (record.type === "MX") {
202
+ payload.content = data;
203
+ payload.priority = record.priority ?? 10;
204
+ }
205
+ else if (record.type === "SRV") {
206
+ payload.data = {
207
+ priority: record.priority ?? 10,
208
+ weight: record.weight ?? 10,
209
+ port: record.port ?? 5060,
210
+ target: data,
211
+ };
212
+ }
213
+ else if (record.type === "CAA") {
214
+ payload.data = {
215
+ flags: record.flags ?? 0,
216
+ tag: record.tag ?? "issue",
217
+ value: data,
218
+ };
219
+ }
220
+ else {
221
+ payload.content = data;
222
+ payload.proxied = targetProxied;
223
+ }
224
+ if (updateableMatch) {
225
+ consumedRecordIds.add(updateableMatch.id);
226
+ if (dryRun) {
227
+ console.log(` 📝 [PLAN] Update ${record.type} ${targetFullName} → ${data}`);
228
+ }
229
+ else {
230
+ await api.put(`/zones/${this.resolvedId}/dns_records/${updateableMatch.id}`, payload);
231
+ console.log(` 🔄 Updated ${record.type} ${targetFullName} → ${data}`);
232
+ }
233
+ }
234
+ else {
235
+ if (dryRun) {
236
+ console.log(` 📝 [PLAN] Create ${record.type} ${targetFullName} → ${data}`);
237
+ }
238
+ else {
239
+ await api.post(`/zones/${this.resolvedId}/dns_records`, payload);
240
+ console.log(` 🚀 Created ${record.type} ${targetFullName} → ${data}`);
241
+ }
242
+ }
243
+ }
244
+ // Purge stale
245
+ const declaredKeySet = new Set(this.records.map((r) => `${r.type}:${getFullRecordName(r.name, this.domainName)}`));
246
+ for (const r of existingRecords) {
247
+ if (consumedRecordIds.has(r.id))
248
+ continue;
249
+ if (declaredKeySet.has(`${r.type}:${r.name}`)) {
250
+ if (dryRun) {
251
+ console.log(` 📝 [PLAN] Delete stale ${r.type} ${r.name}`);
252
+ }
253
+ else {
254
+ await api.delete(`/zones/${this.resolvedId}/dns_records/${r.id}`);
255
+ console.log(` 🗑️ Deleted stale ${r.type} ${r.name}`);
256
+ }
257
+ }
258
+ }
259
+ await this.deploySidecars();
260
+ return { zone: this.domainName, zoneId: this.resolvedId };
261
+ }
262
+ async destroy() {
263
+ const dryRun = this.isDryRunActive();
264
+ const existing = await this.discoveryPromise;
265
+ const api = getCloudflareApi();
266
+ console.log(`\n🗑️ Destroying Cloudflare DNS Zone "${this.domainName}"...`);
267
+ if (!existing) {
268
+ console.log(` ─ Zone "${this.domainName}" not found`);
269
+ return { destroyed: false };
270
+ }
271
+ if (dryRun) {
272
+ console.log(` 📝 [PLAN] Delete Cloudflare Zone "${this.domainName}" (id=${existing.id})`);
273
+ return { destroyed: this.domainName };
274
+ }
275
+ await api.delete(`/zones/${existing.id}`);
276
+ console.log(` 🗑️ Removed Cloudflare Zone "${this.domainName}" (id=${existing.id})`);
277
+ await this.destroySidecars();
278
+ return { destroyed: this.domainName };
279
+ }
280
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,284 @@
1
+ import { test, describe, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { ZoneBuilder } from "./zone.js";
6
+ import { Config } from "../../core/config.js";
7
+ describe("ZoneBuilder Unit Tests", () => {
8
+ let originalFetch;
9
+ let fetchCalls = [];
10
+ let mockResponses = {};
11
+ beforeEach(() => {
12
+ Config.set({
13
+ dryRun: false,
14
+ providers: {
15
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
16
+ }
17
+ });
18
+ originalFetch = globalThis.fetch;
19
+ fetchCalls = [];
20
+ mockResponses = {};
21
+ globalThis.fetch = async (input, init) => {
22
+ const url = String(input);
23
+ const method = init?.method ?? "GET";
24
+ let body;
25
+ if (init?.body) {
26
+ if (typeof init.body === "string") {
27
+ try {
28
+ body = JSON.parse(init.body);
29
+ }
30
+ catch {
31
+ body = init.body;
32
+ }
33
+ }
34
+ else {
35
+ body = init.body;
36
+ }
37
+ }
38
+ const headers = init?.headers;
39
+ fetchCalls.push({ url, method, body, headers });
40
+ const matchKey = Object.keys(mockResponses).find(key => {
41
+ const [mMethod, mPath] = key.split(" ");
42
+ return method === mMethod && url.endsWith(mPath);
43
+ });
44
+ if (matchKey) {
45
+ const resp = mockResponses[matchKey];
46
+ return {
47
+ ok: resp.status >= 200 && resp.status < 300,
48
+ status: resp.status,
49
+ json: async () => resp.body,
50
+ text: async () => JSON.stringify(resp.body),
51
+ };
52
+ }
53
+ return {
54
+ ok: false,
55
+ status: 404,
56
+ json: async () => ({ errors: [{ message: "Not found" }] }),
57
+ text: async () => "Not found",
58
+ };
59
+ };
60
+ });
61
+ afterEach(() => {
62
+ globalThis.fetch = originalFetch;
63
+ });
64
+ test("discovers zone successfully if it already exists", async () => {
65
+ mockResponses["GET /zones?name=example.com"] = {
66
+ status: 200,
67
+ body: {
68
+ result: [
69
+ { id: "zone-123", name: "example.com", status: "active" }
70
+ ]
71
+ }
72
+ };
73
+ mockResponses["GET /zones/zone-123/dns_records?per_page=100"] = {
74
+ status: 200,
75
+ body: { result: [] }
76
+ };
77
+ const builder = new ZoneBuilder("example.com");
78
+ const result = await builder.deploy();
79
+ assert.strictEqual(result.zoneId, "zone-123");
80
+ assert.strictEqual(builder.resolvedId, "zone-123");
81
+ const posts = fetchCalls.filter(c => c.method === "POST");
82
+ assert.strictEqual(posts.length, 0); // No zone creation
83
+ });
84
+ test("creates zone if it does not exist", async () => {
85
+ mockResponses["GET /zones?name=example.com"] = {
86
+ status: 200,
87
+ body: { result: [] }
88
+ };
89
+ mockResponses["POST /zones"] = {
90
+ status: 200,
91
+ body: { result: { id: "zone-new-456" } }
92
+ };
93
+ const builder = new ZoneBuilder("example.com");
94
+ const result = await builder.deploy();
95
+ assert.strictEqual(result.zoneId, "zone-new-456");
96
+ assert.strictEqual(builder.resolvedId, "zone-new-456");
97
+ const postCall = fetchCalls.find(c => c.method === "POST" && c.url.endsWith("/zones"));
98
+ assert.ok(postCall);
99
+ assert.deepStrictEqual(postCall.body, {
100
+ name: "example.com",
101
+ account: { id: "fake-cf-account" },
102
+ type: "full"
103
+ });
104
+ });
105
+ test("handles dry-run deploy properly", async () => {
106
+ Config.set({
107
+ dryRun: true,
108
+ providers: {
109
+ cloudflare: { token: "fake-cf-token", accountId: "fake-cf-account" }
110
+ }
111
+ });
112
+ mockResponses["GET /zones?name=example.com"] = {
113
+ status: 200,
114
+ body: { result: [] }
115
+ };
116
+ const builder = new ZoneBuilder("example.com");
117
+ const result = await builder.deploy();
118
+ assert.strictEqual(result.zoneId, "PENDING");
119
+ const posts = fetchCalls.filter(c => c.method === "POST");
120
+ assert.strictEqual(posts.length, 0);
121
+ });
122
+ test("reconciles records correctly (creates missing, updates out of date, deletes stale)", async () => {
123
+ mockResponses["GET /zones?name=example.com"] = {
124
+ status: 200,
125
+ body: {
126
+ result: [{ id: "zone-123", name: "example.com" }]
127
+ }
128
+ };
129
+ mockResponses["GET /zones/zone-123/dns_records?per_page=100"] = {
130
+ status: 200,
131
+ body: {
132
+ result: [
133
+ // Perfect match - should be skipped
134
+ { id: "rec-1", type: "A", name: "www.example.com", content: "1.1.1.1", proxied: true },
135
+ // Out of date - should be updated (content mismatch)
136
+ { id: "rec-2", type: "CNAME", name: "blog.example.com", content: "old.example.com", proxied: false },
137
+ // Duplicate/stale record of a declared name/type - should be deleted
138
+ { id: "rec-3", type: "A", name: "www.example.com", content: "8.8.8.8", proxied: true }
139
+ ]
140
+ }
141
+ };
142
+ mockResponses["PUT /zones/zone-123/dns_records/rec-2"] = {
143
+ status: 200,
144
+ body: { result: { id: "rec-2" } }
145
+ };
146
+ mockResponses["POST /zones/zone-123/dns_records"] = {
147
+ status: 200,
148
+ body: { result: { id: "rec-4" } }
149
+ };
150
+ mockResponses["DELETE /zones/zone-123/dns_records/rec-3"] = {
151
+ status: 200,
152
+ body: {}
153
+ };
154
+ const builder = new ZoneBuilder("example.com");
155
+ // rec-1
156
+ builder.pointer("www", "1.1.1.1", true);
157
+ // rec-2 (updated)
158
+ builder.cname("blog", "new.example.com", false);
159
+ // rec-4 (created)
160
+ builder.pointer("api", "2.2.2.2", false);
161
+ await builder.deploy();
162
+ const putCall = fetchCalls.find(c => c.method === "PUT");
163
+ assert.ok(putCall);
164
+ assert.ok(putCall.url.endsWith("/zones/zone-123/dns_records/rec-2"));
165
+ assert.deepStrictEqual(putCall.body, {
166
+ type: "CNAME",
167
+ name: "blog",
168
+ content: "new.example.com",
169
+ ttl: 3600,
170
+ proxied: false
171
+ });
172
+ const postCall = fetchCalls.find(c => c.method === "POST" && c.url.includes("/dns_records"));
173
+ assert.ok(postCall);
174
+ assert.ok(postCall.url.endsWith("/zones/zone-123/dns_records"));
175
+ assert.deepStrictEqual(postCall.body, {
176
+ type: "A",
177
+ name: "api",
178
+ content: "2.2.2.2",
179
+ ttl: 3600,
180
+ proxied: false
181
+ });
182
+ const deleteCall = fetchCalls.find(c => c.method === "DELETE");
183
+ assert.ok(deleteCall);
184
+ assert.ok(deleteCall.url.endsWith("/zones/zone-123/dns_records/rec-3"));
185
+ });
186
+ test("loads records from configuration file (YAML) successfully", async () => {
187
+ mockResponses["GET /zones?name=file-example.com"] = {
188
+ status: 200,
189
+ body: {
190
+ result: [{ id: "zone-555", name: "file-example.com" }]
191
+ }
192
+ };
193
+ mockResponses["GET /zones/zone-555/dns_records?per_page=100"] = {
194
+ status: 200,
195
+ body: { result: [] }
196
+ };
197
+ mockResponses["POST /zones/zone-555/dns_records"] = {
198
+ status: 200,
199
+ body: { result: { id: "rec-new" } }
200
+ };
201
+ const tempYamlPath = path.resolve(process.cwd(), "temp-cf-records.yaml");
202
+ const yamlContent = `
203
+ - name: mail
204
+ type: MX
205
+ value: mail.file-example.com
206
+ priority: 5
207
+ - name: _sip._udp
208
+ type: SRV
209
+ value: sip.file-example.com
210
+ port: 5061
211
+ priority: 20
212
+ weight: 20
213
+ - name: '@'
214
+ type: CAA
215
+ value: letsencrypt.org
216
+ tag: issue
217
+ flags: 0
218
+ `;
219
+ fs.writeFileSync(tempYamlPath, yamlContent, "utf-8");
220
+ try {
221
+ const builder = new ZoneBuilder("file-example.com")
222
+ .record("temp-cf-records.yaml");
223
+ await builder.deploy();
224
+ const mxPost = fetchCalls.find(c => c.method === "POST" && c.body?.type === "MX");
225
+ assert.ok(mxPost);
226
+ assert.deepStrictEqual(mxPost.body, {
227
+ type: "MX",
228
+ name: "mail",
229
+ content: "mail.file-example.com",
230
+ ttl: 3600,
231
+ priority: 5
232
+ });
233
+ const srvPost = fetchCalls.find(c => c.method === "POST" && c.body?.type === "SRV");
234
+ assert.ok(srvPost);
235
+ assert.deepStrictEqual(srvPost.body, {
236
+ type: "SRV",
237
+ name: "_sip._udp",
238
+ ttl: 3600,
239
+ data: {
240
+ priority: 20,
241
+ weight: 20,
242
+ port: 5061,
243
+ target: "sip.file-example.com"
244
+ }
245
+ });
246
+ const caaPost = fetchCalls.find(c => c.method === "POST" && c.body?.type === "CAA");
247
+ assert.ok(caaPost);
248
+ assert.deepStrictEqual(caaPost.body, {
249
+ type: "CAA",
250
+ name: "@",
251
+ ttl: 3600,
252
+ data: {
253
+ flags: 0,
254
+ tag: "issue",
255
+ value: "letsencrypt.org"
256
+ }
257
+ });
258
+ }
259
+ finally {
260
+ if (fs.existsSync(tempYamlPath))
261
+ fs.unlinkSync(tempYamlPath);
262
+ }
263
+ });
264
+ test("destroys zone successfully if exists", async () => {
265
+ mockResponses["GET /zones?name=example.com"] = {
266
+ status: 200,
267
+ body: {
268
+ result: [
269
+ { id: "zone-123", name: "example.com" }
270
+ ]
271
+ }
272
+ };
273
+ mockResponses["DELETE /zones/zone-123"] = {
274
+ status: 200,
275
+ body: {}
276
+ };
277
+ const builder = new ZoneBuilder("example.com");
278
+ const result = await builder.destroy();
279
+ assert.deepStrictEqual(result, { destroyed: "example.com" });
280
+ const deleteCall = fetchCalls.find(c => c.method === "DELETE");
281
+ assert.ok(deleteCall);
282
+ assert.ok(deleteCall.url.endsWith("/zones/zone-123"));
283
+ });
284
+ });
@@ -0,0 +1 @@
1
+ export {};