fix(certificates): продлевать по подтверждённому сроку и проверить реальный импорт

This commit is contained in:
Denozordec committed 2026-10-15 00:05:03 +07:00
1 parent 0e86628d58
commit 9178ff2ccb
19 files changed
+431 -13

No files matched your search

@@ -3,6 +3,7 @@ import { createCertificateRenewCollector } from "./certificate-renew-collector.j
import { getServerRowByIdString } from "./certificates-service.js"
import type { CertificateIssueJobRow } from "../db/schema.js"
import type { RosCertificateRow } from "./certificate-parse.js"
import { readCertificateRowsWithClock } from "./certificates-state.js"
function fixture() {
const settings = { enabled: true, intervalSec: 300, renewBeforeDays: 30, lastCollectedAt: null, lastDurationMs: null, lastError: null, updatedAt: new Date().toISOString() }
@@ -12,14 +13,15 @@ function fixture() {
let job: CertificateIssueJobRow | null = null, outcome: CertificateIssueJobRow["status"] = "done", readFailure = false, active = false
let resultPatch: Partial<CertificateIssueJobRow> = {}, missingResult = false, targetGate: Promise<void> | undefined
let reads = 0, creates = 0, issues = 0
let readResult: unknown
const collector = createCertificateRenewCollector({
getSettings: async () => settings, getTargets: async () => { await targetGate; return [target] }, hasActiveJob: async () => active, getServer: async () => server,
read: async () => { reads++; if (readFailure) throw new Error("RouterOS offline"); return rows },
read: async () => { reads++; if (readFailure) throw new Error("RouterOS offline"); return readResult ?? rows },
createJob: async input => { creates++; job = { ...input, serverId: Number(input.serverId), status: "queued", step: "queued", source: "scheduler", requestedAt: new Date().toISOString(), startedAt: null, finishedAt: null, error: null } },
runJob: async () => { issues++; assert(job); job.status = outcome; job.step = outcome; job.error = outcome === "failed" ? "import refused" : null },
getJob: async () => missingResult || !job ? null : { ...job, ...resultPatch }, touchMeta: async () => {}, event: async () => {},
})
return { collector, settings, target, counts: () => ({ reads, creates, issues }), setServer: (v: typeof server) => { server = v }, setRows: (v: typeof rows) => { rows = v }, setOutcome: (v: typeof outcome) => { outcome = v }, failRead: () => { readFailure = true }, setActive: () => { active = true }, rows: () => rows, patchResult: (v: typeof resultPatch) => { resultPatch = v }, hideResult: () => { missingResult = true }, blockTargets: (v: Promise<void>) => { targetGate = v } }
return { collector, settings, target, counts: () => ({ reads, creates, issues }), setServer: (v: typeof server) => { server = v }, setRows: (v: typeof rows) => { rows = v }, setReadResult: (v: unknown) => { readResult = v }, setOutcome: (v: typeof outcome) => { outcome = v }, failRead: () => { readFailure = true }, setActive: () => { active = true }, rows: () => rows, patchResult: (v: typeof resultPatch) => { resultPatch = v }, hideResult: () => { missingResult = true }, blockTargets: (v: Promise<void>) => { targetGate = v } }
}
for (const outcome of ["done", "failed", "running", "queued"] as const) {
@@ -60,3 +62,27 @@ for (const patch of [{ serverId: 2 }, { certName: "another" }, { source: "manual
assert.equal((await f.collector.run()).skipped, true); release(); assert.equal((await first).renewed, 1); assert.equal(f.counts().issues, 1)
}
console.log("Renewal: only matching done counts as renewed; failed/pending/disabled/unknown/ambiguous/read error never dispatch or claim success; invalid IDs do not query DB")
for (const offsets of [["+02:00","+02:00"],["+02:00","+01:00"],[null,"+02:00"]]) {
const f=fixture(), native=f.rows().map(row=>({...row,
"invalid-before":new Date(Date.now()-86400000+7200000).toISOString().slice(0,19).replace("T"," "),
"invalid-after":new Date(Date.now()+172800000+7200000).toISOString().slice(0,19).replace("T"," ")}))
let i=0
f.setReadResult(await readCertificateRowsWithClock(async()=>native,async()=>({"gmt-offset":offsets[i++]})))
const result=await f.collector.run(), known=offsets[0]!==null&&offsets[0]===offsets[1]
assert.equal(result.renewed,known?1:0);assert.equal(f.counts().issues,known?1:0);assert.equal(f.counts().creates,known?1:0)
assert.equal(result.targets?.[0].daysLeft,known?2:null)
}
console.log("Native clock → canonical state → renewal: stable offset issues once; missing/changed offset never creates a job")
{
const f=fixture();f.setRows(f.rows().map(row=>({...row,flags:undefined})))
assert.equal((await f.collector.run()).renewed,1);assert.equal(f.counts().issues,1)
}
for(const kind of ["missing-start","future-start","reversed-window"]) {
const f=fixture();f.setRows(f.rows().map(row=>({...row,flags:undefined,
"invalid-before":kind==="missing-start"?undefined:new Date(Date.now()+86400000).toISOString(),
"invalid-after":kind==="reversed-window"?new Date(Date.now()-86400000).toISOString():row["invalid-after"]})))
assert.equal((await f.collector.run()).renewed,0);assert.equal(f.counts().issues,0)
}
console.log("Missing REST revocation flags do not prevent date-based renewal; unknown/future/reversed validity dates remain guarded")
@@ -127,7 +127,9 @@ export function createCertificateRenewCollector(overrides: Partial<typeof defaul
}
item.daysLeft = hit.daysLeft
if (hit.daysLeft === null || hit.status === "unknown") {
// REST may omit revocation flags even for an installed signed leaf.
// Renewal depends on confirmed validity dates, not a claim about revocation.
if (hit.daysLeft === null || !hit.validFrom || !hit.validUntil || Date.parse(hit.validFrom) > Date.now() || Date.parse(hit.validFrom) > Date.parse(hit.validUntil)) {
item.action = "skipped"
item.message = "Срок или состояние сертификата не подтверждены. Обновление не выполнялось."
snapshot.skippedTargets += 1