Files
MikrotikManager/shared/api/scheduler.test.ts
T

51 lines
5.2 KiB
TypeScript

import assert from "node:assert/strict"
import { test } from "node:test"
import { getSchedulerStatus, listSchedulerRuns, refreshScheduler, runSchedulerJobNow } from "./scheduler"
const base = "https://api.example.test"
const job = { jobKey: "backups", enabled: false, intervalSec: 60, running: false, lastFinishedAt: null, lastStatus: null, lastDurationMs: 0, lastError: null }
const snapshot = { v: 1, job: "backups", sampledAt: "2026-10-01T00:00:00Z", due: true, created: 1, failures: 0, pruned: 0 }
const row = { id: "run-1", jobKey: "backups", startedAt: snapshot.sampledAt, finishedAt: snapshot.sampledAt, durationMs: 0, status: "ok", error: null, resultJson: snapshot }
const ack = { ok: true, result: { status: "ok", error: null, result: snapshot }, status: { jobs: [job] } }
const json = (body: unknown) => new Response(JSON.stringify(body))
test("known empty, false, zero and null stay explicit; missing/duplicate status is rejected", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async () => json({ jobs: [] }); assert.deepEqual(await getSchedulerStatus(base), { jobs: [] })
globalThis.fetch = async () => json({ jobs: [job] }); const status = await getSchedulerStatus(base); assert.equal(status.jobs[0].enabled, false); assert.equal(status.jobs[0].lastDurationMs, 0); assert.equal(status.jobs[0].lastStatus, null)
for (const body of [{}, { jobs: [job, job] }, { jobs: [{ ...job, running: undefined }] }, { jobs: [{ ...job, lastStatus: "unknown" }] }, { jobs: [{ ...job, intervalSec: 0 }] }]) { globalThis.fetch = async () => json(body); await assert.rejects(() => getSchedulerStatus(base)) }
} finally { globalThis.fetch = original }
})
test("run catalogue retains JSONB and normalizes real PostgreSQL timestamps without invented snapshots", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async () => json({ runs: [{ ...row, startedAt: "2026-10-01 00:00:00+00", resultJson: snapshot }] }); const result = await listSchedulerRuns(base, { jobKey: "backups" }); assert.equal(result.runs[0].startedAt, "2026-10-01T00:00:00.000Z"); assert.deepEqual(result.runs[0].resultJson, snapshot)
for (const body of [{}, { runs: [row, row] }, { runs: [{ ...row, durationMs: -1 }] }, { runs: [{ ...row, jobKey: "traffic" }] }]) { globalThis.fetch = async () => json(body); await assert.rejects(() => listSchedulerRuns(base, { jobKey: "backups" })) }
} finally { globalThis.fetch = original }
})
test("successful and partial receipts preserve files, event error and independent status read failure", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async () => json(ack); assert.equal((await runSchedulerJobNow(base, "backups")).ok, true)
globalThis.fetch = async () => json({ ok: false, result: { status: "error", error: "S3 unavailable", eventError: "event unavailable", result: snapshot }, status: null, statusError: "status unavailable" }); const result = await runSchedulerJobNow(base, "backups"); assert.equal(result.ok, false); assert.equal(result.result.result.job, "backups"); assert.equal(result.result.eventError, "event unavailable"); assert.equal(result.statusError, "status unavailable")
} finally { globalThis.fetch = original }
})
test("empty, mismatched and contradictory POST acknowledgments never confirm a run or retry", async () => {
const original = globalThis.fetch
try {
for (const body of [{}, { ...ack, result: { ...ack.result, result: { ...snapshot, errors: ["failed"] } } }, { ...ack, result: { ...ack.result, result: { ...snapshot, uploadFailures: 1 } } }, { ...ack, ok: false }, { ...ack, result: { ...ack.result, result: null } }, { ...ack, result: { ...ack.result, result: { v: 1, job: "traffic", sampledAt: snapshot.sampledAt, servers: [] } } }, { ...ack, status: null }, { ...ack, result: { ...ack.result, status: "error", error: null } }]) {
let posts = 0; globalThis.fetch = async (_url, init) => { assert.equal(init?.method, "POST"); posts++; return json(body) }; await assert.rejects(() => runSchedulerJobNow(base, "backups")); assert.equal(posts, 1)
}
let posts = 0; globalThis.fetch = async () => { posts++; throw Error("lost acknowledgment") }; await assert.rejects(() => runSchedulerJobNow(base, "backups"), /lost acknowledgment/); assert.equal(posts, 1)
} finally { globalThis.fetch = original }
})
test("invalid input fails before HTTP, caller cancellation is preserved and refresh needs confirmation", async () => {
const original = globalThis.fetch
try {
let calls = 0; globalThis.fetch = async () => { calls++; return json({}) }
await assert.rejects(() => runSchedulerJobNow(base, "invalid" as never)); await assert.rejects(() => listSchedulerRuns(base, { limit: 0 })); assert.equal(calls, 0)
const controller = new AbortController(); controller.abort(); globalThis.fetch = async (_url, init) => { assert.equal(init?.signal?.aborted, true); throw controller.signal.reason }; await assert.rejects(() => getSchedulerStatus(base, { signal: controller.signal }))
globalThis.fetch = async () => json({ ok: false, status: { jobs: [] } }); await assert.rejects(() => refreshScheduler(base))
globalThis.fetch = async () => json({ ok: true, status: { jobs: [] } }); assert.equal((await refreshScheduler(base)).ok, true)
} finally { globalThis.fetch = original }
})