39 lines
5.8 KiB
TypeScript
39 lines
5.8 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import Fastify from "fastify"
|
|
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
|
import { createIpsecRoutes } from "./ipsec.js"
|
|
import { MikrotikClient, MikrotikError } from "../services/mikrotik.js"
|
|
import { fetchIpsecState } from "../services/ipsec-live.js"
|
|
const server = { id: 1, name: "test", host: "192.0.2.1", enabled: true } as Parameters<typeof fetchIpsecState>[0]
|
|
const original = MikrotikClient.fromServer
|
|
let writes = 0, records = 0, fault = "", certificateFixture = false, removedCertificates = 0
|
|
const identities: Record<string,string>[] = [{ ".id": "*I", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:alice", "mode-config": "mc-ipsec-alice", comment: "MikrotikManager:ipsec user=Alice" }, { ".id": "*OTHER", peer: "ike", "auth-method": "pre-shared-key", "remote-id": "key-id:other", "mode-config": "mc-ipsec-other", comment: "MikrotikManager:ipsec user=Other" }]
|
|
MikrotikClient.fromServer = () => ({ get: async (p: string) => certificateFixture && p === "/certificate" ? [{ ".id": "*CERT", name: "ipsec-user-shared", "common-name": "shared", "key-usage": "tls-client" }] : p === "/ip/ipsec/peer" ? [{ ".id": "*P", name: "ike", "exchange-mode": "ike2" }] : p === "/ip/ipsec/identity" ? identities : p === "/ip/ipsec/mode-config" ? [{ ".id": "*MC", name: "mc-ipsec-alice", address: "198.18.81.250" }, { ".id": "*OTHERMC", name: "mc-ipsec-other", address: "198.18.81.249" }] : [], removeCertificate: async () => { writes++; removedCertificates++ }, put: async () => { writes++ }, post: async () => { writes++ }, delete: async () => { writes++ }, patch: async (path: string, body: Record<string,string>) => { writes++; if (fault === "partial" && writes === 2) throw new MikrotikError(403, path, "rejected"); Object.assign(identities.find(i => path.endsWith(i[".id"]))!, body); if (fault === "lost-ack") throw Error("socket closed after application") } }) as unknown as MikrotikClient
|
|
const app = Fastify(); app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
|
|
await app.register(createIpsecRoutes({ findServer: async () => server, recordRevision: async () => { records++; return {created:false,revision:{} as never} } }), { prefix: "/api" })
|
|
const patch = (payload: Record<string, unknown>) => app.inject({ method: "PATCH", url: "/api/ipsec/users/1/identity:*I", payload })
|
|
try {
|
|
for (const body of [{name:"New",staticIp:"198.18.81.249"},{name:"Other"},{remoteId:"key-id:other"}]) { writes=0; const r=await patch(body); assert.equal(r.statusCode,409); assert.equal(writes,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Alice") }
|
|
writes=records=0; fault="partial"; let r=await patch({name:"New",psk:"new-test-secret"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"partial"); assert.equal(r.json().confirmedWrites,1); assert.equal(records,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=New")
|
|
writes=records=0; fault="lost-ack"; r=await patch({name:"Unknown"}); assert.equal(r.statusCode,502); assert.equal(r.json().state,"unknown"); assert.equal(r.json().confirmedWrites,0); assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Unknown"); assert.equal(records,0)
|
|
writes=records=0; fault=""; r=await patch({remoteId:"fresh"}); assert.equal(r.statusCode,200); assert.equal(identities[0]!["remote-id"],"key-id:fresh"); assert.equal(writes,1); assert.equal(records,1)
|
|
|
|
|
|
|
|
// A same-named certificate is a separate object, not a binding inferred from a PSK label.
|
|
certificateFixture=true;identities.splice(0,identities.length,{".id":"*UNBOUND",peer:"ike","auth-method":"pre-shared-key","remote-id":"key-id:unbound",comment:"MikrotikManager:ipsec user=shared"});
|
|
writes=records=removedCertificates=0;
|
|
let deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*UNBOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,1);assert.equal(removedCertificates,0);assert.equal(records,1);
|
|
// Explicit certificate binding still permits deletion when no other references exist.
|
|
identities.splice(0,identities.length,{".id":"*BOUND",peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=shared"});
|
|
writes=records=removedCertificates=0;
|
|
deletion=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*BOUND"});assert.equal(deletion.statusCode,200);assert.equal(writes,2);assert.equal(removedCertificates,1);assert.equal(records,1);
|
|
certificateFixture=true;identities.splice(0,identities.length,...["*FIRST","*SECOND"].map((id,n)=>({".id":id,peer:"ike","auth-method":"digital-signature","remote-certificate":"ipsec-user-shared",comment:"MikrotikManager:ipsec user=Shared"+n})));
|
|
for(const method of ["PATCH","DELETE"] as const){writes=records=0;const r=await app.inject({method,url:"/api/ipsec/users/1/cert:*CERT",...(method==="PATCH"?{payload:{name:"Wrong"}}:{})});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0)}
|
|
writes=records=0;r=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*FIRST"});assert.equal(r.statusCode,409);assert.equal(writes,0);assert.equal(records,0);
|
|
writes=records=0;r=await app.inject({method:"PATCH",url:"/api/ipsec/users/1/identity:*FIRST",payload:{name:"Selected"}});assert.equal(r.statusCode,200);assert.equal(writes,1);assert.equal(identities[0]!.comment,"MikrotikManager:ipsec user=Selected");assert.equal(identities[1]!.comment,"MikrotikManager:ipsec user=Shared1");
|
|
writes=records=0;r=await app.inject({method:"DELETE",url:"/api/ipsec/users/1/identity:*FIRST?removeCertificate=false"});assert.equal(r.statusCode,200);assert.equal(writes,1);assert.equal(records,1);
|
|
} finally { await app.close(); MikrotikClient.fromServer=original }
|
|
console.log("IPsec HTTP mutations: no-write conflicts, acknowledged partial failure, applied-but-lost ack and remote ID normalization")
|
|
|