Files
cloudflare-domain-manager/apps/api/test/certificates.test.ts
T
DenozordecandCursor 278a6b64b4
Build and Push CFDM Docker Image / build-and-push (push) Successful in 1m59s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s
fix(certificates): не мониторить SSL без health-check с TLS verify
Биндинги с выключенным health-check (imsk/mmsk) исключаются из авто-мониторинга; sticky footer в редактировании сервиса.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 18:09:51 +07:00

544 lines
14 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { repos } from "@cfdm/db";
import {
CERT_ERROR,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
} from "@cfdm/shared";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import * as certificateService from "../src/services/certificate-service.js";
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("certificates", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("auto mode does not monitor DNS-only domain", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"dns-only.example.com",
"cf-zone-dns",
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "connection refused",
});
const checkRes = await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(checkRes.statusCode).toBe(200);
const certs = repos.listCertificates(testApp.db);
expect(certs.find((c) => c.hostname === domain.zone_name)).toBeUndefined();
await testApp.close();
});
it("monitors host with enabled service binding", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"app.example.com",
"cf-zone-app",
);
const service = repos.createService(testApp.db, "Web", "web");
repos.setServiceEnabled(testApp.db, service.id, true);
const binding = repos.insertBinding(
testApp.db,
domain.id,
service.id,
"api",
null,
);
repos.updateBindingLbConfig(testApp.db, binding.id, {
health_check_enabled: true,
health_check_verify_tls: true,
});
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt,
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const certs = repos.listCertificates(testApp.db);
expect(certs.some((c) => c.hostname === "api.app.example.com")).toBe(true);
expect(certs.some((c) => c.hostname === "app.example.com")).toBe(false);
await testApp.close();
});
it("does not monitor binding when health-check is off", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"rkns.example.com",
"cf-zone-imsk",
);
const service = repos.createService(testApp.db, "Cname", "cname");
repos.setServiceEnabled(testApp.db, service.id, true);
const binding = repos.insertBinding(
testApp.db,
domain.id,
service.id,
"imsk",
null,
);
repos.setBindingCnameTarget(testApp.db, binding.id, "ihome.rkns.example.com");
repos.updateBindingLbConfig(testApp.db, binding.id, {
health_check_enabled: false,
});
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
"imsk.rkns.example.com",
null,
CERT_ERROR,
"stale",
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some(
(c) => c.hostname === "imsk.rkns.example.com",
),
).toBe(false);
expect(certificateService.checkHostname).not.toHaveBeenCalled();
const listRes = await testApp.inject({
method: "GET",
url: "/api/v1/certificates",
headers,
});
expect(listRes.statusCode).toBe(200);
expect(
(listRes.json() as { hostname: string }[]).some(
(c) => c.hostname === "imsk.rkns.example.com",
),
).toBe(false);
await testApp.close();
});
it("does not monitor host when service is disabled", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"off.example.com",
"cf-zone-off",
);
const service = repos.createService(testApp.db, "Off", "off");
repos.insertBinding(testApp.db, domain.id, service.id, "@", null);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some((c) => c.hostname === domain.zone_name),
).toBe(false);
await testApp.close();
});
it("required apex is monitored without bindings", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"required.example.com",
"cf-zone-req",
);
repos.updateDomain(testApp.db, domain.id, {
group_id: null,
status: "active",
cert_monitoring: CERT_MONITOR_REQUIRED,
});
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some(
(c) => c.hostname === domain.zone_name,
),
).toBe(true);
await testApp.close();
});
it("skipped apex removes stale certificate on check", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"skipped.example.com",
"cf-zone-skip",
);
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
domain.zone_name,
null,
CERT_ERROR,
"stale",
);
repos.updateDomain(testApp.db, domain.id, {
group_id: null,
status: "active",
cert_monitoring: CERT_MONITOR_SKIPPED,
});
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "should not be called",
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(repos.listCertificates(testApp.db)).toHaveLength(0);
expect(certificateService.checkHostname).not.toHaveBeenCalled();
await testApp.close();
});
it("TLS failure on monitored host is stored as error", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"broken.example.com",
"cf-zone-broken",
);
repos.updateDomain(testApp.db, domain.id, {
group_id: null,
status: "active",
cert_monitoring: CERT_MONITOR_REQUIRED,
});
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "certificate has expired",
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const cert = repos.listCertificates(testApp.db)[0];
expect(cert?.status).toBe(CERT_ERROR);
expect(cert?.last_error).toBeTruthy();
await testApp.close();
});
it("skips SSL monitoring when group health is on without TLS verify", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"rkns.example.com",
"cf-zone-rkns",
);
const group = repos.createServiceGroup(
testApp.db,
"TG Proxy",
"vpn",
null,
"gt.rkns.example.com",
{
health_check_enabled: true,
health_check_type: "http",
health_check_port: 443,
health_check_verify_tls: false,
},
);
const service = repos.createService(testApp.db, "Node", "node");
repos.setServiceEnabled(testApp.db, service.id, true);
repos.setServiceGroup(testApp.db, service.id, group.id);
repos.insertBinding(testApp.db, domain.id, service.id, "rutg", null);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const certs = repos.listCertificates(testApp.db);
expect(certs.some((c) => c.hostname === "gt.rkns.example.com")).toBe(false);
expect(certs.some((c) => c.hostname === "rutg.rkns.example.com")).toBe(
false,
);
expect(certificateService.checkHostname).not.toHaveBeenCalled();
await testApp.close();
});
it("monitors group hosts when health TLS verify is on", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"ok.example.com",
"cf-zone-ok",
);
const group = repos.createServiceGroup(
testApp.db,
"LB",
"vpn",
null,
"lb.ok.example.com",
{
health_check_enabled: true,
health_check_type: "http",
health_check_port: 443,
health_check_verify_tls: true,
},
);
const service = repos.createService(testApp.db, "Edge", "edge");
repos.setServiceEnabled(testApp.db, service.id, true);
repos.setServiceGroup(testApp.db, service.id, group.id);
repos.insertBinding(testApp.db, domain.id, service.id, "edge", null);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const certs = repos.listCertificates(testApp.db);
expect(certs.some((c) => c.hostname === "lb.ok.example.com")).toBe(true);
expect(certs.some((c) => c.hostname === "edge.ok.example.com")).toBe(true);
await testApp.close();
});
it("required mode still monitors when group skips TLS verify", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"force.example.com",
"cf-zone-force",
);
repos.updateDomain(testApp.db, domain.id, {
group_id: null,
status: "active",
cert_monitoring: CERT_MONITOR_REQUIRED,
});
repos.createServiceGroup(
testApp.db,
"Proxy",
"vpn",
null,
"force.example.com",
{
health_check_enabled: true,
health_check_verify_tls: false,
},
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some(
(c) => c.hostname === domain.zone_name,
),
).toBe(true);
await testApp.close();
});
it("GET /certificates prunes stale rows without running check", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"stale-list.example.com",
"cf-zone-stale-list",
);
const group = repos.createServiceGroup(
testApp.db,
"Stale",
"vpn",
null,
"gt.stale-list.example.com",
{
health_check_enabled: true,
health_check_verify_tls: false,
},
);
const service = repos.createService(testApp.db, "S", "s");
repos.setServiceEnabled(testApp.db, service.id, true);
repos.setServiceGroup(testApp.db, service.id, group.id);
repos.insertBinding(testApp.db, domain.id, service.id, "rutg", null);
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
"gt.stale-list.example.com",
null,
CERT_ERROR,
"stale group domain",
);
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
"rutg.stale-list.example.com",
null,
CERT_ERROR,
"stale binding",
);
expect(repos.listCertificates(testApp.db)).toHaveLength(2);
const listRes = await testApp.inject({
method: "GET",
url: "/api/v1/certificates",
headers,
});
expect(listRes.statusCode).toBe(200);
expect(listRes.json()).toEqual([]);
await testApp.close();
});
});