mirror of
https://github.com/YuzuZensai/Cloudflare-DDNS-Updater.git
synced 2026-07-29 17:00:56 +00:00
🧪 test: add coverage for Updater
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
|
import type { CloudflareConfig, ZoneConfig } from "../libs/config-validation";
|
||||||
|
|
||||||
|
const cfGetRecord = mock();
|
||||||
|
const cfCreateRecord = mock();
|
||||||
|
const cfUpdateRecord = mock();
|
||||||
|
const cfConstructorArgs: Array<[string, string]> = [];
|
||||||
|
|
||||||
|
mock.module("../libs/cloudflare-api", () => ({
|
||||||
|
default: class {
|
||||||
|
constructor(token: string, zoneId: string) {
|
||||||
|
cfConstructorArgs.push([token, zoneId]);
|
||||||
|
}
|
||||||
|
getRecord = cfGetRecord;
|
||||||
|
createRecord = cfCreateRecord;
|
||||||
|
updateRecord = cfUpdateRecord;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getCurrentIPv4 = mock();
|
||||||
|
const getCurrentIPv6 = mock();
|
||||||
|
|
||||||
|
mock.module("../libs/public-ip", () => ({
|
||||||
|
getCurrentIPv4,
|
||||||
|
getCurrentIPv6,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const loggerInfo = mock();
|
||||||
|
const loggerError = mock();
|
||||||
|
|
||||||
|
mock.module("../libs/logger", () => ({
|
||||||
|
default: { info: loggerInfo, error: loggerError, log: mock() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getConfig = mock();
|
||||||
|
|
||||||
|
mock.module("./configuration", () => ({
|
||||||
|
default: { getConfig, init: mock(), loadConfig: mock() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
function makeZone(overrides: Partial<ZoneConfig> = {}): ZoneConfig {
|
||||||
|
return {
|
||||||
|
id: "zone-id",
|
||||||
|
type: "A",
|
||||||
|
name: "example.com",
|
||||||
|
content: "{CURRENT_IPv4}",
|
||||||
|
ttl: 300,
|
||||||
|
proxied: true,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCloudflareConfig(overrides: Partial<CloudflareConfig> = {}): CloudflareConfig {
|
||||||
|
return {
|
||||||
|
token: "some-token",
|
||||||
|
updateInterval: 60,
|
||||||
|
zone: [makeZone()],
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function freshUpdater() {
|
||||||
|
const { default: Updater } = await import(`./updater?t=${Date.now()}-${Math.random()}`);
|
||||||
|
return Updater as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Updater.init", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getConfig.mockReset();
|
||||||
|
loggerInfo.mockReset();
|
||||||
|
loggerError.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs an error and stops when no configuration is found", async () => {
|
||||||
|
getConfig.mockReturnValue(undefined);
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
updater.start = mock();
|
||||||
|
|
||||||
|
updater.init();
|
||||||
|
|
||||||
|
expect(loggerError).toHaveBeenCalledWith("No configuration found");
|
||||||
|
expect(updater.start).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips invalid entries but keeps and starts valid ones", async () => {
|
||||||
|
getConfig.mockReturnValue([makeCloudflareConfig(), { not: "valid" }]);
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
updater.start = mock();
|
||||||
|
|
||||||
|
updater.init();
|
||||||
|
|
||||||
|
expect(updater.config).toEqual([makeCloudflareConfig()]);
|
||||||
|
expect(loggerError).toHaveBeenCalledWith(expect.stringContaining("Invalid configuration"));
|
||||||
|
expect(loggerInfo).toHaveBeenCalledWith("Loaded 1 cloudflare configs");
|
||||||
|
expect(updater.start).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Updater.update", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cfGetRecord.mockReset();
|
||||||
|
cfCreateRecord.mockReset();
|
||||||
|
cfUpdateRecord.mockReset();
|
||||||
|
cfConstructorArgs.length = 0;
|
||||||
|
getCurrentIPv4.mockReset();
|
||||||
|
getCurrentIPv6.mockReset();
|
||||||
|
loggerInfo.mockReset();
|
||||||
|
loggerError.mockReset();
|
||||||
|
|
||||||
|
getCurrentIPv4.mockResolvedValue("1.2.3.4");
|
||||||
|
getCurrentIPv6.mockResolvedValue("::1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves an {ENV_TOKEN:*} placeholder from the environment", async () => {
|
||||||
|
process.env.MY_CF_TOKEN = "resolved-token";
|
||||||
|
cfGetRecord.mockResolvedValue([]);
|
||||||
|
cfCreateRecord.mockResolvedValue({ id: "new" });
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig({ token: "{ENV_TOKEN:MY_CF_TOKEN}" }));
|
||||||
|
|
||||||
|
expect(cfConstructorArgs[0][0]).toBe("resolved-token");
|
||||||
|
delete process.env.MY_CF_TOKEN;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs an error and stops when the placeholder env var is missing", async () => {
|
||||||
|
delete process.env.MISSING_CF_TOKEN;
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig({ token: "{ENV_TOKEN:MISSING_CF_TOKEN}" }));
|
||||||
|
|
||||||
|
expect(loggerError).toHaveBeenCalledWith("Environment variable MISSING_CF_TOKEN not found");
|
||||||
|
expect(cfGetRecord).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs an error and stops when the ip lookup fails", async () => {
|
||||||
|
getCurrentIPv4.mockRejectedValue(new Error("network down"));
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(loggerError).toHaveBeenCalledWith(expect.stringContaining("Unable to fetch ip address"));
|
||||||
|
expect(cfGetRecord).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips a zone when fetching its records fails", async () => {
|
||||||
|
cfGetRecord.mockRejectedValue(new Error("boom"));
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(loggerError).toHaveBeenCalledWith("Unable to get records for zone zone-id");
|
||||||
|
expect(cfCreateRecord).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates a record when none exist for the zone", async () => {
|
||||||
|
cfGetRecord.mockResolvedValue([]);
|
||||||
|
cfCreateRecord.mockResolvedValue({ id: "new" });
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(cfCreateRecord).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ type: "A", name: "example.com", content: "1.2.3.4" }),
|
||||||
|
);
|
||||||
|
expect(loggerInfo).toHaveBeenCalledWith(expect.stringContaining("Created"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates the record when content differs from the current ip", async () => {
|
||||||
|
cfGetRecord.mockResolvedValue([
|
||||||
|
{ id: "record-1", content: "9.9.9.9", ttl: 300, proxied: true },
|
||||||
|
]);
|
||||||
|
cfUpdateRecord.mockResolvedValue({ id: "record-1" });
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(cfUpdateRecord).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ record_id: "record-1", content: "1.2.3.4" }),
|
||||||
|
);
|
||||||
|
expect(loggerInfo).toHaveBeenCalledWith(expect.stringContaining("updated to"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates ttl/proxied when content matches but settings differ", async () => {
|
||||||
|
cfGetRecord.mockResolvedValue([
|
||||||
|
{ id: "record-1", content: "1.2.3.4", ttl: 60, proxied: false },
|
||||||
|
]);
|
||||||
|
cfUpdateRecord.mockResolvedValue({ id: "record-1" });
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(cfUpdateRecord).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ record_id: "record-1", ttl: 300, proxied: true }),
|
||||||
|
);
|
||||||
|
expect(loggerInfo).toHaveBeenCalledWith(expect.stringContaining("updated proxy status/ttl"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does nothing when the record is already up to date", async () => {
|
||||||
|
cfGetRecord.mockResolvedValue([
|
||||||
|
{ id: "record-1", content: "1.2.3.4", ttl: 300, proxied: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(cfUpdateRecord).not.toHaveBeenCalled();
|
||||||
|
expect(loggerInfo).toHaveBeenCalledWith(expect.stringContaining("already up to date"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs an error when multiple records are found for a zone", async () => {
|
||||||
|
cfGetRecord.mockResolvedValue([
|
||||||
|
{ id: "record-1", content: "1.2.3.4", ttl: 300, proxied: true },
|
||||||
|
{ id: "record-2", content: "1.2.3.4", ttl: 300, proxied: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const updater = await freshUpdater();
|
||||||
|
await updater.update(makeCloudflareConfig());
|
||||||
|
|
||||||
|
expect(loggerError).toHaveBeenCalledWith(expect.stringContaining("Multiple records found"));
|
||||||
|
expect(cfUpdateRecord).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user