import assert from "node:assert/strict" import Fastify from "fastify" import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod" import { backupJobResponseSchema } from "@mmapp/contracts/backups" import { createBackupsRoutes } from "./backups.js" import { permissionForRequest } from "../lib/permissions.js" const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler) let created = 0, restored = 0, eventCalls = 0, restoreFail = false await app.register(createBackupsRoutes({ listServers: async () => [{ id: 1 }, { id: 2 }], create: async id => { created++; if (id === "2") throw new Error("RouterOS unavailable"); return { id: "backup-1", serverId: 1, serverName: "router", filename: "config.rsc", sizeBytes: 0, createdAt: "2026-10-01T00:00:00Z", kind: "manual", storage: "local" } }, restore: async () => { restored++; if (restoreFail) throw new Error("import failed"); return { filename: "config.rsc", serverName: "router" } }, appendEvent: async () => { eventCalls++; throw new Error("event DB unavailable") } }), { prefix: "/api" }) const post = (payload: unknown) => app.inject({ method: "POST", url: "/api/backups/create", payload: payload as Record }) for (const serverIds of [[], ["1", "missing"], ["1", "3"], ["1", "1"], ["0"], ["1junk"], [9007199254740992]]) assert.equal((await post({ serverIds })).statusCode, 400) assert.equal(created, 0) const ack = await post({ serverIds: [1, "2"] }); assert.equal(ack.statusCode, 202) const id = ack.json().jobId for (let attempt = 0; attempt < 20; attempt++) { const observation = await app.inject({ method: "GET", url: "/api/backups/jobs/" + id }) const job = backupJobResponseSchema.parse(observation.json()) if (job.status === "done") { assert.equal(job.completed, 2); assert.equal(job.created.length, 1); assert.deepEqual(job.failures, [{ serverId: "2", error: "RouterOS unavailable" }]); break } await new Promise(resolve => setImmediate(resolve)); assert.ok(attempt < 19) } assert.equal(created, 2); assert.ok(eventCalls >= 2) const requestId = "11111111-1111-4111-8111-111111111111", before = created const repeated = await Promise.all([post({ requestId, serverIds: ["1"], notes: " manual " }), post({ requestId, serverIds: [1], notes: "manual" })]) assert.ok(repeated.every(response => response.statusCode === 202 && response.json().jobId === requestId)) await new Promise(resolve => setImmediate(resolve)); assert.equal(created, before + 1) assert.equal((await post({ requestId, serverIds: ["1"], notes: "manual" })).statusCode, 202); assert.equal(created, before + 1) assert.equal((await post({ requestId, serverIds: ["2"], notes: "manual" })).statusCode, 409) assert.equal((await post({ requestId, serverIds: ["1"], notes: "other" })).statusCode, 409) const success = await app.inject({ method: "POST", url: "/api/backups/backup-1/restore" }); assert.equal(success.statusCode, 200); assert.deepEqual(success.json(), { filename: "config.rsc", serverName: "router" }); assert.equal(restored, 1) restoreFail = true; const failure = await app.inject({ method: "POST", url: "/api/backups/backup-1/restore" }); assert.equal(failure.statusCode, 400); assert.equal(failure.json().error, "import failed") assert.equal(permissionForRequest("POST", "/api/backups/create"), "mm:backups:write"); assert.equal(permissionForRequest("POST", "/api/backups/backup-1/restore"), "mm:backups:write"); assert.equal(permissionForRequest("GET", "/api/backups/jobs/job"), "mm:backups:read") await app.close(); console.log("Backups HTTP: event failure does not relabel created/restore, invalid targets have no side effects, partial results and RBAC")