Files
nxdns/web/src/lib/settingsDiff.test.ts
T

105 lines
3.1 KiB
TypeScript

import { buildSettingsPatch } from "@/lib/settingsDiff";
import type { Settings } from "@/lib/types";
function baseSettings(): Settings {
return {
runtime: { io_backend: "threaded" },
upstream: { connect_timeout_ms: 2000, read_timeout_ms: 3000, total_timeout_ms: 5000 },
dns: { bind_ipv4: "0.0.0.0", bind_ipv6: "::", port: 53, rate_limit: 100, rate_window_seconds: 60 },
blocking: { response: "zero", ttl: 300 },
cache: { size: 10000, negative_ttl_max: 300 },
web: {
enabled: true,
bind: "127.0.0.1",
port: 8080,
session_ttl_hours: 24,
api_rate_limit_per_min: 60,
api_localhost_exempt: true,
sse_max_connections_per_ip: 2,
auth_enabled: true,
},
doh_server: { enabled: false, bind: "0.0.0.0", port: 443, cert_path: "", key_path: "" },
dot_server: { enabled: false, bind: "0.0.0.0", port: 853, cert_path: "", key_path: "" },
edns: { ecs_mode: "strip" },
logging: {
level: "info",
retention_days: 30,
query_log_buffer_max: 10000,
hide_domains: false,
hide_client_ips: false,
output: "stderr",
file_path: "",
max_size_mb: 50,
max_files: 3,
},
disk: { min_free_mb: 100, warn_free_mb: 500 },
blocklist_update: { enabled: true, interval_hours: 24 },
};
}
function edit(mutate: (s: Settings) => void): Settings {
const edited = structuredClone(baseSettings());
mutate(edited);
return edited;
}
test("no changes and no password produces null", () => {
expect(buildSettingsPatch(baseSettings(), baseSettings())).toBeNull();
});
test("an empty password is not a change", () => {
expect(buildSettingsPatch(baseSettings(), baseSettings(), "")).toBeNull();
});
test("a single scalar change patches only its section field", () => {
const edited = edit((s) => {
s.dns.port = 5353;
});
expect(buildSettingsPatch(baseSettings(), edited)).toEqual({ dns: { port: 5353 } });
});
test("changes across sections stay grouped and minimal", () => {
const edited = edit((s) => {
s.logging.level = "debug";
s.logging.retention_days = 7;
s.cache.size = 20000;
});
expect(buildSettingsPatch(baseSettings(), edited)).toEqual({
cache: { size: 20000 },
logging: { level: "debug", retention_days: 7 },
});
});
test("tls listener sections diff like any other", () => {
const edited = edit((s) => {
s.dot_server.enabled = true;
s.dot_server.cert_path = "/etc/nxdns/dot.pem";
});
expect(buildSettingsPatch(baseSettings(), edited)).toEqual({
dot_server: { enabled: true, cert_path: "/etc/nxdns/dot.pem" },
});
});
test("web.auth_enabled is never emitted even when it differs", () => {
const edited = edit((s) => {
s.web.auth_enabled = false;
s.web.port = 9090;
});
expect(buildSettingsPatch(baseSettings(), edited)).toEqual({ web: { port: 9090 } });
});
test("a password alone produces a web-only patch", () => {
expect(buildSettingsPatch(baseSettings(), baseSettings(), "hunter2")).toEqual({
web: { password: "hunter2" },
});
});
test("a password merges into an existing web section diff", () => {
const edited = edit((s) => {
s.web.session_ttl_hours = 48;
});
expect(buildSettingsPatch(baseSettings(), edited, "hunter2")).toEqual({
web: { session_ttl_hours: 48, password: "hunter2" },
});
});