- Changed service creation to default `enabled` to true in the database schema. - Updated service group schema to set default values for `icon`, `domain`, and health check parameters. - Refactored service routes to use `await` for fetching service views, ensuring proper asynchronous handling. - Improved error handling in service groups query to provide clearer feedback on response validation. This commit enhances the overall service management experience by ensuring services are enabled by default and improving the robustness of the service group schema.
169 lines
5.1 KiB
TypeScript
169 lines
5.1 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();
|
|
});
|
|
});
|