quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 51s
quality / api (push) Successful in 43s
CD / quality (push) Successful in 1m43s
CD / publish (push) Successful in 1m38s
- Added functionality to toggle individual IPs for services, allowing for dynamic management of IP health status. - Enhanced the health check configuration in the UI, enabling users to set parameters directly from the settings page. - Updated service views to include IP health tracking, improving visibility into the status of each IP associated with a service. - Refactored relevant components to support the new IP toggling feature, ensuring a seamless user experience. This commit significantly enhances the health management capabilities of services, providing users with more control over IP configurations and health monitoring.
259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { serviceGroupsResponseSchema } from "@cfdm/shared";
|
|
import { repos } from "@cfdm/db";
|
|
import type { CloudflareClient } from "../src/lib/cf-client.js";
|
|
import { buildApp } from "../src/app.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import {
|
|
listGroupViews,
|
|
updateConfig,
|
|
} from "../src/services/service-config-service.js";
|
|
|
|
function mockCf(): CloudflareClient {
|
|
return {
|
|
listDnsRecords: async () => [],
|
|
createDnsRecord: async (_zoneId: string, payload: { type: string; name: string; content: string }) => ({
|
|
id: `cf-${payload.name}`,
|
|
type: payload.type,
|
|
name: payload.name,
|
|
content: payload.content,
|
|
ttl: 1,
|
|
proxied: false,
|
|
}),
|
|
updateDnsRecord: async (
|
|
_zoneId: string,
|
|
id: string,
|
|
payload: { type: string; name: string; content: string },
|
|
) => ({
|
|
id,
|
|
type: payload.type,
|
|
name: payload.name,
|
|
content: payload.content,
|
|
ttl: 1,
|
|
proxied: false,
|
|
}),
|
|
deleteDnsRecord: async () => undefined,
|
|
verifyToken: async () => true,
|
|
listZones: async () => [{ id: "zone-1", name: "example.com", status: "active" }],
|
|
} as unknown as CloudflareClient;
|
|
}
|
|
|
|
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
|
|
const config = loadConfig();
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/auth/login",
|
|
payload: { username: config.adminUsername, password: "admin" },
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
const { token } = res.json() as { token: string };
|
|
return { authorization: `Bearer ${token}` };
|
|
}
|
|
|
|
describe("create service then list groups", () => {
|
|
it("create + updateConfig then listGroupViews parses with shared Zod schema", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const headers = await authHeaders(app);
|
|
const cf = mockCf();
|
|
|
|
repos.createDomain(app.db, null, "example.com", "zone-1");
|
|
const group = repos.createServiceGroup(
|
|
app.db,
|
|
"VPN",
|
|
"vpn",
|
|
null,
|
|
"vpn.example.com",
|
|
);
|
|
|
|
const createRes = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/services",
|
|
headers,
|
|
payload: {
|
|
name: "Panel",
|
|
slug: "panel",
|
|
service_group_id: group.id,
|
|
},
|
|
});
|
|
expect(createRes.statusCode).toBe(200);
|
|
const created = createRes.json() as {
|
|
id: number;
|
|
name: string;
|
|
enabled: boolean;
|
|
};
|
|
expect(created.id).toBeTypeOf("number");
|
|
expect(created.enabled).toBe(true);
|
|
|
|
await updateConfig(app.db, cf, created.id, {
|
|
ips: ["1.2.3.4"],
|
|
service_group_id: group.id,
|
|
domains: [
|
|
{
|
|
fqdn: "panel.example.com",
|
|
target_ips: ["1.2.3.4"],
|
|
target_ip_weights: { "1.2.3.4": 1 },
|
|
target_ip_priorities: { "1.2.3.4": 1 },
|
|
lb_mode: "round_robin",
|
|
health_check_enabled: true,
|
|
health_check_type: "tcp",
|
|
health_check_port: 443,
|
|
health_check_path: null,
|
|
health_check_expected_status: null,
|
|
health_check_interval_sec: 30,
|
|
health_check_timeout_ms: 3000,
|
|
health_check_verify_tls: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
const body = await listGroupViews(app.db);
|
|
const parsed = serviceGroupsResponseSchema.safeParse(body);
|
|
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
|
|
|
|
const listRes = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/service-groups",
|
|
headers,
|
|
});
|
|
expect(listRes.statusCode).toBe(200);
|
|
const httpParsed = serviceGroupsResponseSchema.safeParse(listRes.json());
|
|
expect(httpParsed.success, JSON.stringify(httpParsed.error?.issues)).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
httpParsed.data!.groups
|
|
.find((g) => g.id === group.id)
|
|
?.services.some((s) => s.id === created.id),
|
|
).toBe(true);
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("POST /services returns resolved ServiceView with numeric id", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const headers = await authHeaders(app);
|
|
|
|
const createRes = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/services",
|
|
headers,
|
|
payload: { name: "Bare", slug: "bare" },
|
|
});
|
|
expect(createRes.statusCode).toBe(200);
|
|
const created = createRes.json() as Record<string, unknown>;
|
|
expect(typeof created.id).toBe("number");
|
|
expect(created.name).toBe("Bare");
|
|
expect(created.enabled).toBe(true);
|
|
expect(Array.isArray(created.ips)).toBe(true);
|
|
expect(Array.isArray(created.domains)).toBe(true);
|
|
|
|
const listRes = await app.inject({
|
|
method: "GET",
|
|
url: "/api/v1/service-groups",
|
|
headers,
|
|
});
|
|
expect(listRes.statusCode).toBe(200);
|
|
const parsed = serviceGroupsResponseSchema.safeParse(listRes.json());
|
|
expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true);
|
|
expect(parsed.data!.ungrouped.some((s) => s.slug === "bare")).toBe(true);
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("PATCH /services/:id/ips/toggle keeps IP in pool and removes it from A-binding", async () => {
|
|
const app = await buildApp({
|
|
config: { ...loadConfig(), staticDir: null },
|
|
memory: true,
|
|
});
|
|
const headers = await authHeaders(app);
|
|
const cf = mockCf();
|
|
|
|
repos.createDomain(app.db, null, "example.com", "zone-1");
|
|
const group = repos.createServiceGroup(
|
|
app.db,
|
|
"VPN",
|
|
"vpn",
|
|
null,
|
|
"vpn.example.com",
|
|
);
|
|
|
|
const createRes = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/services",
|
|
headers,
|
|
payload: {
|
|
name: "Panel",
|
|
slug: "panel-ip-toggle",
|
|
service_group_id: group.id,
|
|
},
|
|
});
|
|
expect(createRes.statusCode).toBe(200);
|
|
const created = createRes.json() as { id: number };
|
|
|
|
await updateConfig(app.db, cf, created.id, {
|
|
ips: ["1.2.3.4", "5.6.7.8"],
|
|
service_group_id: group.id,
|
|
domains: [
|
|
{
|
|
fqdn: "panel.example.com",
|
|
target_ips: ["1.2.3.4", "5.6.7.8"],
|
|
target_ip_weights: { "1.2.3.4": 1, "5.6.7.8": 1 },
|
|
target_ip_priorities: { "1.2.3.4": 1, "5.6.7.8": 1 },
|
|
lb_mode: "round_robin",
|
|
health_check_enabled: false,
|
|
health_check_type: "tcp",
|
|
health_check_port: 443,
|
|
health_check_path: null,
|
|
health_check_expected_status: null,
|
|
health_check_interval_sec: 30,
|
|
health_check_timeout_ms: 3000,
|
|
health_check_verify_tls: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
// HTTP toggle uses request.server.cf; disable DNS push so the test
|
|
// does not call the real Cloudflare client.
|
|
repos.setServiceEnabled(app.db, created.id, false);
|
|
|
|
const offRes = await app.inject({
|
|
method: "PATCH",
|
|
url: `/api/v1/services/${created.id}/ips/toggle`,
|
|
headers,
|
|
payload: { ip: "1.2.3.4", enabled: false },
|
|
});
|
|
expect(offRes.statusCode, JSON.stringify(offRes.json())).toBe(200);
|
|
const offView = offRes.json() as {
|
|
ips: string[];
|
|
ip_enabled: Record<string, boolean>;
|
|
};
|
|
expect(offView.ips).toEqual(expect.arrayContaining(["1.2.3.4", "5.6.7.8"]));
|
|
expect(offView.ip_enabled["1.2.3.4"]).toBe(false);
|
|
expect(offView.ip_enabled["5.6.7.8"]).toBe(true);
|
|
|
|
const binding = repos.listBindingsByService(app.db, created.id)[0]!;
|
|
expect(repos.listBindingIps(app.db, binding.id)).toEqual(["5.6.7.8"]);
|
|
|
|
const onRes = await app.inject({
|
|
method: "PATCH",
|
|
url: `/api/v1/services/${created.id}/ips/toggle`,
|
|
headers,
|
|
payload: { ip: "1.2.3.4", enabled: true },
|
|
});
|
|
expect(onRes.statusCode).toBe(200);
|
|
const onView = onRes.json() as { ip_enabled: Record<string, boolean> };
|
|
expect(onView.ip_enabled["1.2.3.4"]).toBe(true);
|
|
expect(repos.listBindingIps(app.db, binding.id)).toEqual(
|
|
expect.arrayContaining(["1.2.3.4", "5.6.7.8"]),
|
|
);
|
|
|
|
await app.close();
|
|
});
|
|
});
|