Files
MikrotikManager/backend/src/services/certificate-issue-execution.test.ts
T

94 lines
6.1 KiB
TypeScript

import assert from "node:assert/strict"
import { test } from "node:test"
import type { CertificateIssueJobRow } from "../db/schema.js"
import { createCertificateIssueRunner } from "./certificate-issue-execution.js"
function fixture() {
const row: CertificateIssueJobRow = { id: "job", serverId: 1, status: "queued", step: "queued", source: "manual", certName: "cert", domainNames: ["test.example"], keyType: "rsa2048", trustStore: "www,api", requestedAt: "2026-10-01T00:00:00Z", startedAt: null, finishedAt: null, error: null }
const writes: Array<{ status?: string; step?: string }> = []
const events: string[] = [], warnings: unknown[] = []
let calls = 0
const deps = {
getJob: async () => row,
getServer: async () => ({ id: 1, name: "router" }),
updateJob: async (_id: string, patch: Parameters<Parameters<typeof createCertificateIssueRunner>[0]["updateJob"]>[1]) => { writes.push(patch); Object.assign(row, patch) },
issue: async (_input: Parameters<Parameters<typeof createCertificateIssueRunner>[0]["issue"]>[0]) => { calls += 1 },
appendEvent: async (input: Parameters<Parameters<typeof createCertificateIssueRunner>[0]["appendEvent"]>[0]) => { events.push(input.eventType) },
warn: (error: unknown) => { warnings.push(error) },
}
return { row, writes, events, warnings, deps, calls: () => calls }
}
test("completed operation remains done when event storage fails", async () => {
const f = fixture(); f.deps.appendEvent = async () => { throw new Error("event unavailable") }
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.row.status, "done"); assert.equal(f.row.error, null); assert.equal(f.calls(), 1); assert.equal(f.warnings.length, 1)
})
test("scheduler start event failure does not prevent the issue", async () => {
const f = fixture(); f.row.source = "scheduler"
f.deps.appendEvent = async () => { throw new Error("event unavailable") }
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.calls(), 1); assert.equal(f.row.status, "done"); assert.equal(f.warnings.length, 2)
})
test("actual import failure remains failed despite event storage failure", async () => {
const f = fixture(); f.deps.issue = async input => { input.onStep?.("cleanup"); throw new Error("RouterOS import failed") }
f.deps.appendEvent = async () => { throw new Error("event unavailable") }
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.row.status, "failed"); assert.equal(f.row.step, "failed"); assert.equal(f.row.error, "RouterOS import failed")
})
test("progress is serialized and flushed before terminal status", async () => {
const f = fixture(); const update = f.deps.updateJob
let release!: () => void
const gate = new Promise<void>(resolve => { release = resolve })
f.deps.updateJob = async (id, patch) => { if (patch.step === "dns_challenge") await gate; await update(id, patch) }
f.deps.issue = async input => { input.onStep?.("dns_challenge"); input.onStep?.("dns_a_sync"); input.onStep?.("import"); input.onStep?.("cleanup") }
const task = createCertificateIssueRunner(f.deps).run("job")
await new Promise(resolve => setImmediate(resolve))
assert.equal(f.row.status, "running"); assert.deepEqual(f.writes.map(write => write.step), ["acme_order"])
release(); await task
assert.deepEqual(f.writes.map(write => write.step), ["acme_order", "dns_challenge", "dns_a_sync", "import", "cleanup", "done"])
})
test("DNS diagnostics and progress failures do not poison terminal state", async () => {
const f = fixture(); const update = f.deps.updateJob
f.deps.updateJob = async (id, patch) => { if (patch.step === "import") throw new Error("progress storage failed"); await update(id, patch) }
f.deps.issue = async input => { input.onStep?.("dns_a_sync"); input.onStep?.("dns_a_sync_warn:DNS unavailable"); input.onStep?.("dns_a_sync_skip_cname:test.example"); input.onStep?.("import"); input.onStep?.("cleanup") }
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.row.status, "done"); assert.equal(f.row.step, "done"); assert.equal(f.warnings.length, 1)
assert.equal(f.events.filter(value => value === "certificates.issue.dns_a_sync").length, 2)
assert.ok(f.writes.every(write => !write.step?.includes(":")))
})
test("setup failure releases process lock and permits a safe queued retry", async () => {
const f = fixture(); let unavailable = true
f.deps.getJob = async () => { if (unavailable) throw new Error("DB unavailable"); return f.row }
const runner = createCertificateIssueRunner(f.deps)
await assert.rejects(runner.run("job"), /DB unavailable/)
unavailable = false; await runner.run("job")
assert.equal(f.calls(), 1); assert.equal(f.row.status, "done")
})
test("done, failed and running records never replay network operations", async () => {
for (const status of ["done", "failed", "running"] as const) {
const f = fixture(); f.row.status = status
await createCertificateIssueRunner(f.deps).run("job")
assert.equal(f.calls(), 0); assert.equal(f.writes.length, 0)
}
})
test("concurrent dispatch runs a queued job once", async () => {
const f = fixture(), runner = createCertificateIssueRunner(f.deps)
await Promise.all([runner.run("job"), runner.run("job")])
assert.equal(f.calls(), 1)
})
test("success persistence failure never writes failed or retries import", async () => {
const f = fixture(); const update = f.deps.updateJob
f.deps.updateJob = async (id, patch) => { if (patch.status === "done") throw new Error("terminal DB unavailable"); await update(id, patch) }
const runner = createCertificateIssueRunner(f.deps)
await assert.rejects(runner.run("job"), /terminal DB unavailable/)
await runner.run("job")
assert.equal(f.calls(), 1); assert.equal(f.row.status, "running"); assert.ok(f.writes.every(write => write.status !== "failed"))
})
test("queue handles rejected setup without unhandled rejection", async () => {
const f = fixture(); f.deps.getJob = async () => { throw new Error("DB unavailable") }
createCertificateIssueRunner(f.deps).queue("job")
await new Promise(resolve => setImmediate(resolve))
assert.equal(f.warnings.length, 1); assert.equal(f.calls(), 0)
})