quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 4s
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / web (push) Successful in 53s
quality / api (push) Successful in 49s
CD / quality (push) Successful in 1m54s
CD / publish (push) Successful in 1m29s
Тип HA уходит в bindings, чтобы схема могла показать резервирование. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
5.9 KiB
TypeScript
208 lines
5.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
cfdmBindingSyncItemSchema,
|
|
type ServiceBindingView,
|
|
} from "@cfdm/shared";
|
|
import {
|
|
resolveBindingIpsForSync,
|
|
resolveLbModeForSync,
|
|
} from "../src/services/vps-tracker-sync.js";
|
|
|
|
function binding(
|
|
partial: Partial<ServiceBindingView> &
|
|
Pick<ServiceBindingView, "id" | "hostname" | "zone_name">,
|
|
): ServiceBindingView {
|
|
return {
|
|
domain_id: 1,
|
|
service_id: 1,
|
|
dns_record_id: null,
|
|
group_id: null,
|
|
group_name: null,
|
|
service_name: "svc",
|
|
service_slug: "svc",
|
|
target_ip: null,
|
|
target_ips: [],
|
|
target_ip_weights: {},
|
|
target_ip_priorities: {},
|
|
cname_target: null,
|
|
lb_mode: "off",
|
|
health_check_enabled: false,
|
|
health_check_type: "tcp",
|
|
health_check_port: null,
|
|
health_check_path: null,
|
|
health_check_expected_status: null,
|
|
health_check_interval_sec: 60,
|
|
health_check_timeout_ms: 3000,
|
|
health_check_verify_tls: true,
|
|
sync_status: null,
|
|
created_at: "",
|
|
updated_at: "",
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
describe("resolveBindingIpsForSync", () => {
|
|
it("uses A-record IPs when present", async () => {
|
|
const a = binding({
|
|
id: 1,
|
|
hostname: "ihome",
|
|
zone_name: "rkns.top",
|
|
target_ips: ["10.0.0.5"],
|
|
});
|
|
const index = { byFqdn: new Map([["ihome.rkns.top", a]]) };
|
|
const ips = await resolveBindingIpsForSync(a, ["9.9.9.9"], index);
|
|
expect(ips).toEqual(["10.0.0.5"]);
|
|
});
|
|
|
|
it("resolves CNAME via local binding chain to VPS IP", async () => {
|
|
const target = binding({
|
|
id: 1,
|
|
hostname: "ihome",
|
|
zone_name: "rkns.top",
|
|
target_ips: ["203.0.113.10"],
|
|
});
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "imsk",
|
|
zone_name: "rkns.top",
|
|
cname_target: "ihome.rkns.top",
|
|
target_ips: [],
|
|
});
|
|
const index = {
|
|
byFqdn: new Map([
|
|
["ihome.rkns.top", target],
|
|
["imsk.rkns.top", cname],
|
|
]),
|
|
};
|
|
const ips = await resolveBindingIpsForSync(cname, [], index);
|
|
expect(ips).toEqual(["203.0.113.10"]);
|
|
});
|
|
|
|
it("resolves short CNAME target relative to zone", async () => {
|
|
const target = binding({
|
|
id: 1,
|
|
hostname: "ihome",
|
|
zone_name: "rkns.top",
|
|
target_ips: ["203.0.113.11"],
|
|
});
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "imsk",
|
|
zone_name: "rkns.top",
|
|
cname_target: "ihome",
|
|
target_ips: [],
|
|
});
|
|
const index = {
|
|
byFqdn: new Map([
|
|
["ihome.rkns.top", target],
|
|
["imsk.rkns.top", cname],
|
|
]),
|
|
};
|
|
const ips = await resolveBindingIpsForSync(cname, [], index);
|
|
expect(ips).toEqual(["203.0.113.11"]);
|
|
});
|
|
|
|
it("falls back to service IPs when CNAME target unknown locally and DNS fails", async () => {
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "imsk",
|
|
zone_name: "rkns.top",
|
|
cname_target: "definitely-not-resolvable-xyz.invalid",
|
|
target_ips: [],
|
|
});
|
|
const index = { byFqdn: new Map([["imsk.rkns.top", cname]]) };
|
|
const ips = await resolveBindingIpsForSync(cname, ["198.51.100.7"], index);
|
|
expect(ips).toEqual(["198.51.100.7"]);
|
|
});
|
|
|
|
it("does not treat CNAME hostname in target_ips as an IP", async () => {
|
|
const target = binding({
|
|
id: 1,
|
|
hostname: "ihome",
|
|
zone_name: "rkns.top",
|
|
target_ips: ["203.0.113.10"],
|
|
});
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "imsk",
|
|
zone_name: "rkns.top",
|
|
cname_target: "ihome.rkns.top",
|
|
// как в прод: JOIN dns_record кладёт CNAME content в target_ip → target_ips
|
|
target_ips: ["ihome.rkns.top"],
|
|
});
|
|
const index = {
|
|
byFqdn: new Map([
|
|
["ihome.rkns.top", target],
|
|
["imsk.rkns.top", cname],
|
|
]),
|
|
};
|
|
const ips = await resolveBindingIpsForSync(cname, [], index);
|
|
expect(ips).toEqual(["203.0.113.10"]);
|
|
});
|
|
|
|
it("treats missing target_ips as empty instead of throwing", async () => {
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "imsk",
|
|
zone_name: "rkns.top",
|
|
cname_target: "ihome.rkns.top",
|
|
});
|
|
delete (cname as { target_ips?: string[] }).target_ips;
|
|
const index = { byFqdn: new Map([["imsk.rkns.top", cname]]) };
|
|
const ips = await resolveBindingIpsForSync(cname, ["198.51.100.9"], index);
|
|
expect(ips).toEqual(["198.51.100.9"]);
|
|
});
|
|
|
|
it("prefers service IPs over empty CNAME resolution chain", async () => {
|
|
const cname = binding({
|
|
id: 2,
|
|
hostname: "mhome",
|
|
zone_name: "rkns.top",
|
|
cname_target: "macloud.rkns.top",
|
|
target_ips: ["macloud.rkns.top"],
|
|
});
|
|
const index = { byFqdn: new Map([["mhome.rkns.top", cname]]) };
|
|
const ips = await resolveBindingIpsForSync(cname, ["203.0.113.55"], index);
|
|
expect(ips).toEqual(["203.0.113.55"]);
|
|
});
|
|
});
|
|
|
|
describe("resolveLbModeForSync", () => {
|
|
it("prefers binding lb_mode", () => {
|
|
expect(resolveLbModeForSync("failover", "round_robin")).toBe("failover");
|
|
});
|
|
|
|
it("falls back to service group lb_mode", () => {
|
|
expect(resolveLbModeForSync("off", "weighted")).toBe("weighted");
|
|
expect(resolveLbModeForSync(undefined, "round_robin")).toBe("round_robin");
|
|
});
|
|
|
|
it("returns undefined when neither is a known mode", () => {
|
|
expect(resolveLbModeForSync("off", "off")).toBeUndefined();
|
|
expect(resolveLbModeForSync(null, undefined)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("cfdmBindingSyncItemSchema lbMode", () => {
|
|
const base = {
|
|
bindingId: 1,
|
|
serviceId: 10,
|
|
serviceName: "VPN",
|
|
serviceSlug: "vpn",
|
|
fqdn: "vpn.example.com",
|
|
zoneName: "example.com",
|
|
hostname: "vpn",
|
|
ips: ["203.0.113.10"],
|
|
};
|
|
|
|
it("accepts optional lbMode on sync payload", () => {
|
|
const parsed = cfdmBindingSyncItemSchema.parse({ ...base, lbMode: "failover" });
|
|
expect(parsed.lbMode).toBe("failover");
|
|
});
|
|
|
|
it("accepts payload without lbMode (legacy)", () => {
|
|
const parsed = cfdmBindingSyncItemSchema.parse(base);
|
|
expect(parsed.lbMode).toBeUndefined();
|
|
});
|
|
});
|