Files
MikrotikManager/backend/src/services/wireguard-export.test.ts
T

47 lines
5.5 KiB
TypeScript

import assert from "node:assert/strict"
import { test } from "node:test"
import { renderWireguardExport, wireguardExportRequestSchema, type WireguardExportRows } from "./wireguard-export.js"
const serverKey=Buffer.alloc(32,11).toString("base64"), publicKey=Buffer.alloc(32,12).toString("base64"), peerKey=Buffer.alloc(32,13).toString("base64"), peerPrivate=Buffer.alloc(32,14).toString("base64")
const fixture=():WireguardExportRows=>({interfaces:[{".id":"*wg9",name:"wg-test","listen-port":"13231",mtu:"1420",disabled:"false","private-key":serverKey,"public-key":publicKey}],bindings:[{".id":"*if3",name:"wg-test",type:"wg"}],peers:[{".id":"*p",interface:"*if3","public-key":peerKey,"private-key":peerPrivate,"allowed-address":"192.0.2.2/32","endpoint-address":"2001:db8::2","endpoint-port":"13231","persistent-keepalive":"0",disabled:"false","client-address":"192.0.2.2/32","client-dns":"1.1.1.1"}],addresses:[{".id":"*a",interface:"*if3",address:"192.0.2.1/32",disabled:"false",dynamic:"false"}]})
const request=(format:"rsc"|"conf"|"peer-conf",includePrivateKey=false)=>wireguardExportRequestSchema.parse({serverId:"143",interfaceName:"wg-test",format,includePrivateKey})
const render=(format:"rsc"|"conf"|"peer-conf",rows=fixture(),include=false)=>renderWireguardExport(request(format,include),rows,{name:"fixture",host:"192.0.2.9"}).content.toString()
test("export preserves independently joined interface identities and respects private-key opt-in for both formats",()=>{
for(const format of ["rsc","conf"] as const){const plain=render(format);assert.ok(!plain.includes(serverKey));assert.ok(!plain.includes(peerPrivate));assert.ok(plain.includes(peerKey));assert.ok(plain.includes("192.0.2.1/32"));const secret=render(format,fixture(),true);assert.ok(secret.includes(serverKey));assert.ok(!secret.includes(peerPrivate))}
assert.equal(request("rsc").serverId,143);assert.equal(wireguardExportRequestSchema.parse({serverId:143,interfaceName:"wg-test",format:"conf"}).includePrivateKey,false)
const missing=fixture();missing.bindings=[];assert.throws(()=>render("rsc",missing))
const wrong=fixture();wrong.peers[0].interface="*wg9";assert.throws(()=>render("rsc",wrong),"WG catalog ID cannot substitute for the separately observed generic ID")
})
test("peer-conf has client routing placeholders and never exposes server or generated client keys",()=>{
const value=render("peer-conf",fixture(),true)
assert.ok(value.includes(publicKey));assert.ok(value.includes("# AllowedIPs ="));assert.ok(!value.includes(serverKey));assert.ok(!value.includes(peerPrivate));assert.ok(value.includes("Address = 192.0.2.2/32"));assert.ok(value.includes("Endpoint = 192.0.2.9:13231"))
assert.throws(()=>renderWireguardExport({...request("peer-conf"),peerId:"*missing"},fixture(),{name:"fixture",host:"192.0.2.9"}))
assert.deepEqual(renderWireguardExport({...request("peer-conf"),peerId:"143:*p"},fixture(),{name:"fixture",host:"192.0.2.9"}),renderWireguardExport({...request("peer-conf"),peerId:"*p"},fixture(),{name:"fixture",host:"192.0.2.9"}))
})
test("disabled peer flags survive RSC and nonrepresentable conf/address/PSK state is rejected",()=>{
const disabled=fixture();disabled.peers[0].disabled="true";assert.ok(render("rsc",disabled).includes("disabled=yes"));assert.throws(()=>render("conf",disabled))
for(const mutate of [(rows:WireguardExportRows)=>{rows.addresses.push({...rows.addresses[0],".id":"*a2",address:"192.0.2.3/32"})},(rows:WireguardExportRows)=>{rows.addresses[0].dynamic="true"},(rows:WireguardExportRows)=>{rows.addresses[0].advertise="true"},(rows:WireguardExportRows)=>{rows.peers[0]["preshared-key"]=peerPrivate},(rows:WireguardExportRows)=>{delete rows.peers[0].disabled},(rows:WireguardExportRows)=>{rows.interfaces[0].mtu="unknown"}]){const rows=fixture();mutate(rows);assert.throws(()=>render("rsc",rows))}
})
test("unexpected secret reflection and invalid unbounded request values fail without embedding keys",()=>{
const rows=fixture();rows.peers[0].comment=peerPrivate
assert.throws(()=>render("conf",rows),(error:Error)=>!error.message.includes(peerPrivate))
assert.equal(wireguardExportRequestSchema.safeParse({serverId:143,interfaceName:"wg\nunsafe",format:"rsc"}).success,false)
assert.equal(wireguardExportRequestSchema.safeParse({serverId:143,interfaceName:"wg",format:"rsc",arbitrary:"field"}).success,false)
assert.equal(wireguardExportRequestSchema.safeParse({serverId:true,interfaceName:"wg",format:"rsc"}).success,false)
})
test("multiline comments remain representable and safe private-key modes are preserved",()=>{
const rows=fixture();rows.interfaces[0].comment="line one\nline two";rows.peers[0].comment="peer line\nsecond line";rows.peers[0]["private-key"]="none"
const rsc=render("rsc",rows);assert.ok(rsc.includes("private-key=\"none\"")||rsc.includes("private-key=none"));assert.ok(!rsc.includes(serverKey));assert.ok(!rsc.includes(peerPrivate))
assert.doesNotThrow(()=>render("conf",rows))
})
test("native duration keepalives use the same observed codec as peer mutations",()=>{
for(const value of ["25s","00:00:25","0s","1m5s"]){
const rows=fixture();rows.peers[0]["persistent-keepalive"]=value
const seconds=value==="0s"?0:value==="1m5s"?65:25
for(const format of ["rsc","conf","peer-conf"] as const){const result=render(format,rows);assert.ok(result.includes(format==="rsc"?`persistent-keepalive=${seconds}`:`PersistentKeepalive = ${seconds}`))}
}
for(const value of ["65536s","1.5s","unknown"]){const rows=fixture();rows.peers[0]["persistent-keepalive"]=value;assert.throws(()=>render("rsc",rows))}
})