quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 7s
quality / web (push) Skipped
quality / docker-check (push) Skipped
quality / api (push) Successful in 48s
CD / quality (push) Successful in 58s
CD / publish (push) Successful in 1m35s
cf_record_id только как кэш-подсказка: при удалении/обновлении сначала list + match type/name/content, иначе create или no-op. Co-authored-by: Cursor <[email protected]>
221 lines
6.1 KiB
TypeScript
221 lines
6.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createMemoryDb, repos, runMigrations } from "@cfdm/db";
|
|
import { SYNC_SYNCED } from "@cfdm/shared";
|
|
import type { CloudflareClient } from "../src/lib/cf-client.js";
|
|
import * as dnsService from "../src/services/dns-service.js";
|
|
import { updateConfig } from "../src/services/service-config-service.js";
|
|
|
|
type CfRec = {
|
|
id: string;
|
|
type: string;
|
|
name: string;
|
|
content: string;
|
|
ttl: number;
|
|
proxied: boolean;
|
|
};
|
|
|
|
function setupDb() {
|
|
const { db, sqlite } = createMemoryDb();
|
|
runMigrations(sqlite);
|
|
return db;
|
|
}
|
|
|
|
describe("DNS identity by name + content", () => {
|
|
it("deletes by name+IP when cached cf_record_id is stale/missing in CF", async () => {
|
|
const db = setupDb();
|
|
const remote: CfRec[] = [
|
|
{
|
|
id: "cf-live",
|
|
type: "A",
|
|
name: "nsgt.example.com",
|
|
content: "130.49.213.176",
|
|
ttl: 1,
|
|
proxied: false,
|
|
},
|
|
];
|
|
const deleted: string[] = [];
|
|
|
|
const cf = {
|
|
listDnsRecords: async () => [...remote],
|
|
createDnsRecord: async () => {
|
|
throw new Error("create should not run");
|
|
},
|
|
updateDnsRecord: async () => {
|
|
throw new Error("update should not run");
|
|
},
|
|
deleteDnsRecord: async (_zoneId: string, id: string) => {
|
|
deleted.push(id);
|
|
const idx = remote.findIndex((r) => r.id === id);
|
|
if (idx >= 0) remote.splice(idx, 1);
|
|
},
|
|
verifyToken: async () => true,
|
|
listZones: async () => [],
|
|
} as unknown as CloudflareClient;
|
|
|
|
const domain = repos.createDomain(db, null, "example.com", "zone-1");
|
|
const local = repos.insertDnsRecord(
|
|
db,
|
|
domain.id,
|
|
"A",
|
|
"nsgt.example.com",
|
|
"130.49.213.176",
|
|
1,
|
|
false,
|
|
null,
|
|
SYNC_SYNCED,
|
|
"local",
|
|
"cf-stale-gone", // not present in Cloudflare
|
|
);
|
|
|
|
await dnsService.deleteRecord(db, cf, domain.id, local.id);
|
|
|
|
expect(deleted).toEqual(["cf-live"]);
|
|
expect(repos.listDnsByDomain(db, domain.id)).toEqual([]);
|
|
expect(remote).toEqual([]);
|
|
});
|
|
|
|
it("delete is no-op success when record already removed outside CFDM", async () => {
|
|
const db = setupDb();
|
|
const cf = {
|
|
listDnsRecords: async () => [],
|
|
deleteDnsRecord: async () => {
|
|
throw new Error("Record does not exist");
|
|
},
|
|
verifyToken: async () => true,
|
|
listZones: async () => [],
|
|
} as unknown as CloudflareClient;
|
|
|
|
const domain = repos.createDomain(db, null, "example.com", "zone-1");
|
|
const local = repos.insertDnsRecord(
|
|
db,
|
|
domain.id,
|
|
"A",
|
|
"nsgt.example.com",
|
|
"130.49.213.176",
|
|
1,
|
|
false,
|
|
null,
|
|
SYNC_SYNCED,
|
|
"local",
|
|
"cf-already-gone",
|
|
);
|
|
|
|
await expect(
|
|
dnsService.deleteRecord(db, cf, domain.id, local.id),
|
|
).resolves.toBeUndefined();
|
|
expect(repos.listDnsByDomain(db, domain.id)).toEqual([]);
|
|
});
|
|
|
|
it("updateConfig can remove extra FQDN when CF id is stale", async () => {
|
|
const db = setupDb();
|
|
const remote: CfRec[] = [
|
|
{
|
|
id: "cf-gt",
|
|
type: "A",
|
|
name: "gt.example.com",
|
|
content: "130.49.213.176",
|
|
ttl: 1,
|
|
proxied: false,
|
|
},
|
|
{
|
|
id: "cf-nsgt-live",
|
|
type: "A",
|
|
name: "nsgt.example.com",
|
|
content: "130.49.213.176",
|
|
ttl: 1,
|
|
proxied: false,
|
|
},
|
|
];
|
|
const deleted: string[] = [];
|
|
|
|
const cf = {
|
|
listDnsRecords: async () => [...remote],
|
|
createDnsRecord: async (
|
|
_zoneId: string,
|
|
payload: { type: string; name: string; content: string },
|
|
) => {
|
|
const rec: CfRec = {
|
|
id: `cf-new-${remote.length}`,
|
|
type: payload.type,
|
|
name: payload.name,
|
|
content: payload.content,
|
|
ttl: 1,
|
|
proxied: false,
|
|
};
|
|
remote.push(rec);
|
|
return rec;
|
|
},
|
|
updateDnsRecord: async (
|
|
_zoneId: string,
|
|
id: string,
|
|
payload: { type: string; name: string; content: string },
|
|
) => {
|
|
const idx = remote.findIndex((r) => r.id === id);
|
|
if (idx < 0) throw new Error("Record does not exist");
|
|
const rec: CfRec = {
|
|
id,
|
|
type: payload.type,
|
|
name: payload.name,
|
|
content: payload.content,
|
|
ttl: 1,
|
|
proxied: false,
|
|
};
|
|
remote[idx] = rec;
|
|
return rec;
|
|
},
|
|
deleteDnsRecord: async (_zoneId: string, id: string) => {
|
|
const idx = remote.findIndex((r) => r.id === id);
|
|
if (idx < 0) throw new Error("Record does not exist");
|
|
deleted.push(id);
|
|
remote.splice(idx, 1);
|
|
},
|
|
verifyToken: async () => true,
|
|
listZones: async () => [
|
|
{ id: "zone-1", name: "example.com", status: "active" },
|
|
],
|
|
} as unknown as CloudflareClient;
|
|
|
|
repos.createDomain(db, null, "example.com", "zone-1");
|
|
const service = repos.createService(db, "Main TG", "tg-pr");
|
|
repos.setServiceEnabled(db, service.id, true);
|
|
|
|
await updateConfig(db, cf, service.id, {
|
|
ips: ["130.49.213.176"],
|
|
domains: [
|
|
{ fqdn: "gt.example.com", target_ips: ["130.49.213.176"] },
|
|
{ fqdn: "nsgt.example.com", target_ips: ["130.49.213.176"] },
|
|
],
|
|
});
|
|
|
|
// Poison cached id on extra binding's DNS row.
|
|
const nsgtBinding = repos
|
|
.listBindingsByService(db, service.id)
|
|
.find((b) => b.hostname === "nsgt")!;
|
|
const nsgtRecords = repos.listRecordsForBinding(db, nsgtBinding.id);
|
|
for (const row of nsgtRecords) {
|
|
repos.updateDnsFields(
|
|
db,
|
|
row.id,
|
|
row.record_type,
|
|
row.name,
|
|
row.content,
|
|
row.ttl,
|
|
row.proxied,
|
|
row.priority,
|
|
SYNC_SYNCED,
|
|
"cf-stale-nsgt",
|
|
null,
|
|
);
|
|
}
|
|
|
|
const view = await updateConfig(db, cf, service.id, {
|
|
ips: ["130.49.213.176"],
|
|
domains: [{ fqdn: "gt.example.com", target_ips: ["130.49.213.176"] }],
|
|
});
|
|
|
|
expect(view.domains.map((d) => d.fqdn)).toEqual(["gt.example.com"]);
|
|
expect(deleted).toContain("cf-nsgt-live");
|
|
expect(remote.some((r) => r.name.includes("nsgt"))).toBe(false);
|
|
});
|
|
});
|