105 lines
3.1 KiB
TypeScript
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" },
|
|
});
|
|
});
|