94 lines
6.1 KiB
TypeScript
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)
|
|
})
|