/*
RFC-0753: unit tests for Cloudflare API client — pagination, retry, update, delete.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0753: initial Cloudflare API client tests for updateDnsRecord, deleteDnsRecord, pagination, retry.
*/
import { test, expect, vi, beforeEach, afterEach } from "vitest";
import {
listDnsRecords,
createDnsRecord,
updateDnsRecord,
deleteDnsRecord,
} from "./cloudflare-api.ts";
const ZONE_ID = "test-zone-id";
const API_TOKEN = "test-api-token";
function mockResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
test("listDnsRecords: paginates through all pages", async () => {
const fetchMock = vi.fn();
fetchMock
.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: [
{
id: "r1",
type: "A",
name: "a.example.com",
content: "1.2.3.4",
proxied: true,
priority: null,
},
],
result_info: { page: 1, per_page: 50, total_pages: 2, count: 1, total_count: 2 },
}),
)
.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: [
{
id: "r2",
type: "A",
name: "b.example.com",
content: "5.6.7.8",
proxied: false,
priority: null,
},
],
result_info: { page: 2, per_page: 50, total_pages: 2, count: 1, total_count: 2 },
}),
);
vi.stubGlobal("fetch", fetchMock);
const records = await listDnsRecords(ZONE_ID, API_TOKEN);
expect(records).toHaveLength(2);
expect(records[0].id).toBe("r1");
expect(records[1].id).toBe("r2");
expect(fetchMock).toHaveBeenCalledTimes(2);
});
test("listDnsRecords: single page when total_pages is 1", async () => {
const fetchMock = vi.fn();
fetchMock.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: [
{
id: "r1",
type: "A",
name: "a.example.com",
content: "1.2.3.4",
proxied: true,
priority: null,
},
],
result_info: { page: 1, per_page: 50, total_pages: 1, count: 1, total_count: 1 },
}),
);
vi.stubGlobal("fetch", fetchMock);
const records = await listDnsRecords(ZONE_ID, API_TOKEN);
expect(records).toHaveLength(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
test("createDnsRecord: accepts all record types", async () => {
const fetchMock = vi.fn();
fetchMock.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: {
id: "new-r1",
type: "TXT",
name: "_dmarc",
content: "v=DMARC1; p=quarantine",
proxied: false,
priority: null,
},
}),
);
vi.stubGlobal("fetch", fetchMock);
const result = await createDnsRecord(ZONE_ID, API_TOKEN, {
type: "TXT",
name: "_dmarc",
content: "v=DMARC1; p=quarantine",
proxied: false,
});
expect(result.id).toBe("new-r1");
expect(result.type).toBe("TXT");
});
test("updateDnsRecord: sends PUT with record body", async () => {
const fetchMock = vi.fn();
fetchMock.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: {
id: "r1",
type: "A",
name: "a.example.com",
content: "10.0.0.1",
proxied: true,
priority: null,
},
}),
);
vi.stubGlobal("fetch", fetchMock);
const result = await updateDnsRecord(ZONE_ID, API_TOKEN, "r1", {
type: "A",
name: "a.example.com",
content: "10.0.0.1",
proxied: true,
});
expect(result.id).toBe("r1");
expect(fetchMock).toHaveBeenCalledTimes(1);
const call = fetchMock.mock.calls[0];
expect(call[1].method).toBe("PUT");
});
test("deleteDnsRecord: sends DELETE and succeeds", async () => {
const fetchMock = vi.fn();
fetchMock.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: {},
}),
);
vi.stubGlobal("fetch", fetchMock);
await deleteDnsRecord(ZONE_ID, API_TOKEN, "r1");
expect(fetchMock).toHaveBeenCalledTimes(1);
const call = fetchMock.mock.calls[0];
expect(call[1].method).toBe("DELETE");
});
test("deleteDnsRecord: throws on API error", async () => {
const fetchMock = vi.fn();
fetchMock.mockResolvedValueOnce(
mockResponse(200, {
success: false,
errors: [{ code: 1003, message: "Record does not exist" }],
messages: [],
result: {},
}),
);
vi.stubGlobal("fetch", fetchMock);
await expect(deleteDnsRecord(ZONE_ID, API_TOKEN, "r1")).rejects.toThrow(
"deleteDnsRecord API errors",
);
});
test("listDnsRecords: retries on 503 then succeeds", async () => {
const fetchMock = vi.fn();
fetchMock
.mockResolvedValueOnce(mockResponse(503, { error: "Service Unavailable" }))
.mockResolvedValueOnce(
mockResponse(200, {
success: true,
errors: [],
messages: [],
result: [
{
id: "r1",
type: "A",
name: "a.example.com",
content: "1.2.3.4",
proxied: true,
priority: null,
},
],
result_info: { page: 1, per_page: 50, total_pages: 1, count: 1, total_count: 1 },
}),
);
vi.stubGlobal("fetch", fetchMock);
const recordsPromise = listDnsRecords(ZONE_ID, API_TOKEN);
await vi.advanceTimersByTimeAsync(2000);
const records = await recordsPromise;
expect(records).toHaveLength(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
});