/** * Tests for src/network/providers/wireguard.js (DC-149). * * Hermetic: execFile and the ENTIRE fs surface are injected stubs backed by * an in-memory map — no child processes, no real files, no network. Same * rigor as __tests__/network-provider-tailscale.test.js. */ 'use strict'; const { createWireGuardProvider } = require('../src/network/providers/wireguard'); const { createTailscaleProvider } = require('../src/network/providers/tailscale'); const { CONTRACT_METHODS } = require('../src/network/provider'); const PRIV = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; // 44-char b64 const PRIV2 = 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB='; const PUB = 'a'.repeat(43) + '='; // satisfies the 44-char wg key shape const PUB2 = 'b'.repeat(43) + '='; const PUB3 = 'c'.repeat(43) + '='; /** * In-memory fs stub: a Map with the sync fs surface the * provider touches. Records nothing else — call assertions go on exec. */ function makeFs(initial = {}) { const files = new Map(Object.entries(initial)); return { files, readFileSync(p) { if (!files.has(p)) { const err = new Error("ENOENT: no such file '" + p + "'"); err.code = 'ENOENT'; throw err; } return files.get(p); }, writeFileSync(p, contents, opts) { files.set(p, contents); if (opts && opts.mode !== undefined) files.set(p + ':mode', String(opts.mode)); }, renameSync(from, to) { if (!files.has(from)) { const err = new Error("ENOENT: no such file '" + from + "'"); err.code = 'ENOENT'; throw err; } files.set(to, files.get(from)); files.delete(from); }, existsSync(p) { return files.has(p); }, mkdirSync() { /* in-memory: dirs are implicit */ }, }; } /** * execFile stub with canned per-binary behavior. Records every call. * Defaults: root uid, `wg genkey`→PRIV, `wg pubkey`→PUB derived from input, * `wg show dcnet0 dump`→iface-only table, wg-quick up/down ok. */ function makeExec({ genkey = PRIV, dump = null, // string stdout for `wg show dcnet0 dump` dumpFails = false, // `wg show` rejects → interface treated as down missing = [], // binaries that fail with ENOENT failQuickUp = false, } = {}) { const calls = []; const fn = async (bin, args, opts = {}) => { calls.push({ bin, args, opts }); if (missing.includes(bin)) { const err = new Error('spawn ' + bin + ' ENOENT'); err.code = 'ENOENT'; throw err; } if (bin === 'wg' && args[0] === 'genkey') return { stdout: genkey + '\n', stderr: '' }; if (bin === 'wg' && args[0] === 'pubkey') { // deterministic: derive from the piped private key so hub identity is stable const input = String(opts.input || ''); return { stdout: (input.includes('BBBB') ? PUB2 : PUB) + '\n', stderr: '' }; } if (bin === 'wg' && args[0] === 'show' && args[2] === 'dump') { if (dumpFails) throw new Error('wg: dcnet0: No such device'); return { stdout: dump !== null ? dump : PRIV + '\t' + PUB + '\t51820\toff\n', stderr: '' }; } if (bin === 'wg' && args[0] === '--version') return { stdout: 'wg v1.0.20210914\n', stderr: '' }; if (bin === 'wg-quick') { if (args[0] === 'up' && failQuickUp) throw new Error('wg-quick: `wg-quick up dcnet0` failed'); if (args[0] === 'down') { return { stdout: '', stderr: '' }; } return { stdout: '', stderr: '' }; } if (bin === 'sysctl') return { stdout: 'net.ipv4.ip_forward = 1\n', stderr: '' }; return { stdout: '', stderr: '' }; }; fn.calls = calls; return fn; } /** Build a provider + captures with sensible defaults. */ function makeProvider(over = {}) { const fs = over.fs || makeFs(over.files || {}); const exec = over.exec || makeExec(over.execOpts || {}); const provider = createWireGuardProvider({ execFileImpl: exec, fsImpl: fs, dataDir: '/data', confPath: '/wg/dcnet0.conf', unitPath: '/sys/dcnet-hub.service', getuid: over.getuid || (() => 0), now: over.now || (() => 1700000000000), }); return { provider, fs, exec }; } /** Public WG dump line for a peer (epoch handshake optional). */ function dumpLine(pubkey, addr, handshakeSec = 0) { return [pubkey, '(none)', addr + '/32', '1.2.3.4:51820', String(handshakeSec), '100', '200', '0'].join('\t'); } beforeEach(() => { jest.clearAllMocks(); }); describe('wireguard provider: contract + capabilities', () => { test('satisfies every CONTRACT_METHODS entry', () => { const { provider } = makeProvider(); for (const m of CONTRACT_METHODS) { expect(typeof provider[m]).toBe('function'); } }); test('id() returns "wireguard"', () => { expect(makeProvider().provider.id()).toBe('wireguard'); }); test('capabilities: invites, perPeerFirewall, selfHosted; NO splitDnsApi', () => { expect(makeProvider().provider.capabilities()).toEqual({ invites: true, splitDnsApi: false, perPeerFirewall: true, selfHosted: true, }); }); }); describe('wireguard provider: dependency checks', () => { test('missing `wg` binary → dependency_missing', async () => { const exec = makeExec({ missing: ['wg'] }); const { provider } = makeProvider({ exec }); await expect(provider.getStatus()).rejects.toMatchObject({ name: 'ProviderError', code: 'dependency_missing', retryable: false, }); }); test('missing `wg-quick` binary → dependency_missing', async () => { const exec = makeExec({ missing: ['wg-quick'] }); const { provider } = makeProvider({ exec }); await expect(provider.ensureHub()).rejects.toMatchObject({ code: 'dependency_missing' }); }); test('non-root uid without CAP_NET_ADMIN → dependency_missing', async () => { const { provider } = makeProvider({ getuid: () => 1000 }); await expect(provider.getStatus()).rejects.toMatchObject({ code: 'dependency_missing', }); }); }); describe('wireguard provider: ensureHub lifecycle', () => { test('first call: generates hub key ONCE, writes conf + unit, ip_forward, brings iface up', async () => { const { provider, fs, exec } = makeProvider(); const res = await provider.ensureHub(); expect(res).toMatchObject({ interface: 'dcnet0', address: '10.53.0.1/16', listenPort: 51820, up: true }); const genkeys = exec.calls.filter((c) => c.bin === 'wg' && c.args[0] === 'genkey'); expect(genkeys).toHaveLength(1); // pubkey derived via stdin (private key never hits argv) const pubcalls = exec.calls.filter((c) => c.bin === 'wg' && c.args[0] === 'pubkey'); expect(pubcalls).toHaveLength(1); expect(pubcalls[0].opts.input).toContain(PRIV); const conf = fs.files.get('/wg/dcnet0.conf'); expect(conf).toContain('[Interface]'); expect(conf).toContain('PrivateKey = ' + PRIV); expect(conf).toContain('Address = 10.53.0.1/16'); expect(conf).toContain('ListenPort = 51820'); expect(fs.files.get('/sys/dcnet-hub.service')).toContain('ExecStart=/usr/bin/wg-quick up dcnet0'); expect(exec.calls.some((c) => c.bin === 'sysctl' && c.args[1] === 'net.ipv4.ip_forward=1')).toBe(true); }); test('idempotent: second call does NOT regenerate the hub key', async () => { const { provider, exec } = makeProvider(); await provider.ensureHub(); await provider.ensureHub(); expect(exec.calls.filter((c) => c.bin === 'wg' && c.args[0] === 'genkey')).toHaveLength(1); }); test('restart: reuses the PrivateKey already in the conf (no genkey)', async () => { const { provider, exec } = makeProvider({ files: { '/wg/dcnet0.conf': '[Interface]\nPrivateKey = ' + PRIV2 + '\nAddress = 10.53.0.1/16\nListenPort = 51820\n' }, }); const res = await provider.ensureHub(); expect(res.up).toBe(true); expect(exec.calls.filter((c) => c.bin === 'wg' && c.args[0] === 'genkey')).toHaveLength(0); }); test('missing /etc/wireguard dir is created (no throw)', async () => { const { provider } = makeProvider(); await expect(provider.ensureHub()).resolves.toBeTruthy(); }); test('startHub / stopHub round-trip, stopHub tolerates "not up"', async () => { const { provider, exec } = makeProvider(); await provider.ensureHub(); exec.calls.length = 0; await provider.startHub(); expect(exec.calls.some((c) => c.bin === 'wg-quick' && c.args[0] === 'up')).toBe(true); await provider.stopHub(); expect(exec.calls.some((c) => c.bin === 'wg-quick' && c.args[0] === 'down')).toBe(true); // stop again — down on a non-existent iface must not throw await expect(provider.stopHub()).resolves.toMatchObject({ stopped: 'dcnet0' }); }); }); describe('wireguard provider: address pool', () => { test('deterministic lowest-free allocation, hub address excluded', async () => { const { provider, fs } = makeProvider(); const a = await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); const b = await provider.addPeer({ personId: 'p1', deviceId: 'd2', publicKey: PUB2 }); expect(a.address).toBe('10.53.0.2/32'); expect(b.address).toBe('10.53.0.3/32'); const store = JSON.parse(fs.files.get('/data/network-wg-peers.json')); expect(store.peers.map((p) => p.address)).toEqual(['10.53.0.2/32', '10.53.0.3/32']); }); test('skips taken addresses (gap reuse: lowest-free wins)', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await provider.addPeer({ personId: 'p1', deviceId: 'd2', publicKey: PUB2 }); await provider.removePeer(PUB); // frees 10.53.0.2 const c = await provider.addPeer({ personId: 'p2', deviceId: 'd3', publicKey: PUB3 }); expect(c.address).toBe('10.53.0.2/32'); // lowest free, reused }); test('survives restart: pool re-derived from the store, not a counter', async () => { const shared = makeFs({ '/data/network-wg-peers.json': JSON.stringify({ version: 1, peers: [ { personId: 'p1', deviceId: 'd1', publicKey: PUB, address: '10.53.0.2/32', name: 'laptop', createdAt: '2026-01-01T00:00:00Z', blocked: false }, ], }), }); const exec = makeExec(); const first = createWireGuardProvider({ execFileImpl: exec, fsImpl: shared, dataDir: '/data', confPath: '/wg/dcnet0.conf', unitPath: '/sys/dcnet-hub.service', getuid: () => 0, now: () => 1700000000000, }); const r = await first.addPeer({ personId: 'p2', deviceId: 'd9', publicKey: PUB3 }); expect(r.address).toBe('10.53.0.3/32'); // after the surviving 10.53.0.2 // brand-new provider instance (fresh memory) over the same store const second = createWireGuardProvider({ execFileImpl: makeExec(), fsImpl: shared, dataDir: '/data', confPath: '/wg/dcnet0.conf', unitPath: '/sys/dcnet-hub.service', getuid: () => 0, now: () => 1700000000000, }); const devices = await second.listDevices(); expect(devices.map((d) => d.ip).sort()).toEqual(['10.53.0.2', '10.53.0.3']); }); }); describe('wireguard provider: addPeer', () => { test('writes [Peer] to conf + updates store; never stores a private key', async () => { const { provider, fs } = makeProvider(); const res = await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB, name: 'laptop' }); expect(res).toMatchObject({ deviceId: 'd1', address: '10.53.0.2/32' }); expect(res.hubPublicKey).toBe(PUB); // derived from genkey'd hub private key expect(res.allowedIps).toBe('10.53.0.0/16'); // spec §8.1 split tunnel expect(res.dns).toBe('10.53.0.1'); // spec §8.3 const conf = fs.files.get('/wg/dcnet0.conf'); expect(conf).toContain('[Peer]'); expect(conf).toContain('PublicKey = ' + PUB); expect(conf).toContain('AllowedIPs = 10.53.0.2/32'); expect(conf).toContain('# laptop'); const store = JSON.parse(fs.files.get('/data/network-wg-peers.json')); expect(store.peers).toHaveLength(1); expect(store.peers[0]).toMatchObject({ personId: 'p1', deviceId: 'd1', publicKey: PUB, address: '10.53.0.2/32', name: 'laptop' }); // §8.2: the server stores public keys only expect(conf.includes(PRIV)).toBe(true); // hub key in conf is expected expect(JSON.stringify(store).includes(PRIV)).toBe(false); expect(store.peers[0].privateKey).toBeUndefined(); }); test('apply path: wg-quick down + up (regenerated conf), never syncconf shell form', async () => { const { provider, exec } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); const quick = exec.calls.filter((c) => c.bin === 'wg-quick'); expect(quick.some((c) => c.args[0] === 'down')).toBe(true); expect(quick.some((c) => c.args[0] === 'up')).toBe(true); expect(quick.some((c) => c.args[0] === 'syncconf')).toBe(false); const allCmds = exec.calls.map((c) => c.bin + ' ' + c.args.join(' ')).join(';'); expect(allCmds.includes('wg-quick strip')).toBe(false); }); test('rejects invalid public keys (bad_input)', async () => { const { provider } = makeProvider(); await expect(provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: 'not-a-key' })) .rejects.toMatchObject({ code: 'bad_input' }); await expect(provider.addPeer({ personId: 'p1', deviceId: 'd1' })) .rejects.toMatchObject({ code: 'bad_input' }); }); test('rejects duplicate publicKey (bad_input)', async () => { const { provider } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await expect(provider.addPeer({ personId: 'p2', deviceId: 'd2', publicKey: PUB })) .rejects.toMatchObject({ code: 'bad_input' }); }); }); describe('wireguard provider: removePeer / revokeDevice / removePerson', () => { test('removePeer rewrites conf without the [Peer] and drops the store record', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); const res = await provider.removePeer(PUB); expect(res).toMatchObject({ removed: PUB, address: '10.53.0.2/32' }); const conf = fs.files.get('/wg/dcnet0.conf'); expect(conf).not.toContain('[Peer]'); expect(JSON.parse(fs.files.get('/data/network-wg-peers.json')).peers).toHaveLength(0); }); test('removePeer unknown key → not_found', async () => { const { provider } = makeProvider(); await expect(provider.removePeer(PUB)).rejects.toMatchObject({ code: 'not_found' }); }); test('revokeDevice removes by deviceId', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); const res = await provider.revokeDevice('d1'); expect(res).toMatchObject({ revoked: 'd1', publicKey: PUB }); expect(fs.files.get('/wg/dcnet0.conf')).not.toContain('PublicKey = ' + PUB); expect(JSON.parse(fs.files.get('/data/network-wg-peers.json')).peers).toHaveLength(0); }); test('removePerson deletes peers + records entirely', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await provider.addPeer({ personId: 'p1', deviceId: 'd2', publicKey: PUB2 }); await provider.addPeer({ personId: 'p2', deviceId: 'd3', publicKey: PUB3 }); const res = await provider.removePerson('p1'); expect(res).toEqual({ deleted: 2, count: 2 }); const store = JSON.parse(fs.files.get('/data/network-wg-peers.json')); expect(store.peers).toHaveLength(1); expect(store.peers[0].personId).toBe('p2'); }); }); describe('wireguard provider: blockPerson / unblockPerson', () => { test('block removes peers from conf but KEEPS records; conf keeps no trace', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await provider.addPeer({ personId: 'p2', deviceId: 'd2', publicKey: PUB2 }); const res = await provider.blockPerson('p1'); expect(res).toMatchObject({ blocked: true, count: 1 }); const conf = fs.files.get('/wg/dcnet0.conf'); expect(conf).not.toContain('PublicKey = ' + PUB); expect(conf).toContain('PublicKey = ' + PUB2); // other person untouched const store = JSON.parse(fs.files.get('/data/network-wg-peers.json')); const blocked = store.peers.find((p) => p.publicKey === PUB); expect(blocked).toBeTruthy(); // record kept for unblock expect(blocked.blocked).toBe(true); expect(blocked.address).toBe('10.53.0.2/32'); }); test('unblock re-adds stored peers from the record, same addresses (WG restores immediately)', async () => { const { provider, fs } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await provider.blockPerson('p1'); const res = await provider.unblockPerson('p1'); expect(res).toEqual({ restored: [PUB], count: 1, reauthRequired: false }); const conf = fs.files.get('/wg/dcnet0.conf'); expect(conf).toContain('PublicKey = ' + PUB); expect(conf).toContain('AllowedIPs = 10.53.0.2/32'); // same /32 restored const store = JSON.parse(fs.files.get('/data/network-wg-peers.json')); expect(store.peers[0].blocked).toBe(false); }); test('unblockPerson with nothing blocked → empty restore, still reauthRequired:false', async () => { const { provider } = makeProvider(); const res = await provider.unblockPerson('nobody'); expect(res).toEqual({ restored: [], count: 0, reauthRequired: false }); }); test('tailscale unblockPerson is honest: restored true, reauthRequired true, NO coord call', async () => { const coord = { request: jest.fn(async () => ({})), listDevices: jest.fn(async () => []), listUsers: jest.fn(async () => []), deleteDevice: jest.fn(async () => ({})) }; const ts = createTailscaleProvider({ coordClient: coord }); const res = await ts.unblockPerson('1001'); expect(res).toEqual({ restored: true, reauthRequired: true }); expect(coord.request).not.toHaveBeenCalled(); // no un-expire API exists expect(coord.listDevices).not.toHaveBeenCalled(); }); }); describe('wireguard provider: whois / listDevices / getStatus', () => { test('whois(ip) → peer-store lookup by address (with and without /32)', async () => { const { provider } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB, name: 'laptop' }); await expect(provider.whois('10.53.0.2')).resolves.toMatchObject({ personId: 'p1', deviceId: 'd1', publicKey: PUB, blocked: false, }); await expect(provider.whois('10.53.0.2/32')).resolves.toMatchObject({ personId: 'p1' }); await expect(provider.whois('10.53.9.9')).resolves.toBe(null); await expect(provider.whois('')).rejects.toMatchObject({ code: 'bad_input' }); }); test('listDevices merges store with wg dump handshakes (online window)', async () => { const nowMs = 1700000000000; const freshSec = Math.floor((nowMs - 30 * 1000) / 1000); // 30s ago → online const staleSec = Math.floor((nowMs - 10 * 60 * 1000) / 1000); // 10m ago → offline const exec = makeExec({ dump: PRIV + '\t' + PUB + '\t51820\toff\n' + dumpLine(PUB, '10.53.0.2', freshSec) + '\n' + dumpLine(PUB2, '10.53.0.3', staleSec) + '\n', }); const { provider, fs } = makeProvider({ exec, now: () => nowMs }); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); await provider.addPeer({ personId: 'p2', deviceId: 'd2', publicKey: PUB2 }); const devices = await provider.listDevices(); expect(devices).toHaveLength(2); const d1 = devices.find((d) => d.deviceId === 'd1'); const d2 = devices.find((d) => d.deviceId === 'd2'); expect(d1).toMatchObject({ ip: '10.53.0.2', online: true, userId: 'p1', rx: 100, tx: 200 }); expect(d1.lastSeen).toBe(new Date(freshSec * 1000).toISOString()); expect(d2.online).toBe(false); expect(fs.files.get('/data/network-wg-peers.json')).toBeTruthy(); }); test('getStatus: hub up with per-peer handshake state', async () => { const nowMs = 1700000000000; const freshSec = Math.floor((nowMs - 60 * 1000) / 1000); const exec = makeExec({ dump: PRIV + '\t' + PUB + '\t51820\toff\n' + dumpLine(PUB, '10.53.0.2', freshSec) + '\n' }); const { provider } = makeProvider({ exec, now: () => nowMs }); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); const st = await provider.getStatus(); expect(st).toMatchObject({ provider: 'wireguard', installed: true, connected: true, hubUp: true, interface: 'dcnet0', address: '10.53.0.1/16', listenPort: 51820, hubPublicKey: PUB, peerCount: 1, }); expect(st.peers[0]).toMatchObject({ publicKey: PUB, address: '10.53.0.2/32', online: true, rx: 100, tx: 200 }); }); test('getStatus: iface down (wg show fails) degrades to connected:false, peers offline', async () => { const exec = makeExec({ dumpFails: true }); const { provider } = makeProvider({ exec }); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB }); exec.calls.length = 0; const st = await provider.getStatus(); expect(st).toMatchObject({ provider: 'wireguard', connected: false, hubUp: false }); expect(st.peerCount).toBe(1); // from the store, not wg expect(st.peers[0].online).toBe(false); expect(st.peers[0].latestHandshake).toBe(null); }); test('listPeople projects unique persons with earliest createdAt', async () => { const { provider } = makeProvider(); await provider.addPeer({ personId: 'p1', deviceId: 'd1', publicKey: PUB, name: 'laptop' }); await provider.addPeer({ personId: 'p1', deviceId: 'd2', publicKey: PUB2, name: 'phone' }); await provider.addPeer({ personId: 'p2', deviceId: 'd3', publicKey: PUB3 }); const people = await provider.listPeople(); expect(people).toHaveLength(2); expect(people.map((p) => p.id).sort()).toEqual(['p1', 'p2']); expect(people.every((p) => p.email === null)).toBe(true); }); }); describe('wireguard provider: invites (spec §8.5)', () => { test('createInvite → { inviteUrl: /network/wg/redeem/, id }, 7d TTL, persisted', async () => { const t0 = 1700000000000; const { provider, fs } = makeProvider({ now: () => t0 }); const inv = await provider.createInvite({ role: 'member', email: 'a@b.c' }); expect(inv.id).toMatch(/^wgi_/); expect(inv.inviteUrl).toMatch(/^\/network\/wg\/redeem\/[A-Za-z0-9_-]{32}$/); const store = JSON.parse(fs.files.get('/data/network-wg-invites.json')); expect(store.invites).toHaveLength(1); expect(store.invites[0].expiresAt).toBe(t0 + 7 * 24 * 60 * 60 * 1000); expect(store.invites[0].usedAt).toBe(null); }); test('redeem: single-use — second attempt fails', async () => { const { provider } = makeProvider(); const inv = await provider.createInvite({ role: 'member' }); const token = inv.inviteUrl.split('/').pop(); await expect(provider.redeemInvite(token)).resolves.toMatchObject({ role: 'member' }); await expect(provider.redeemInvite(token)).rejects.toMatchObject({ code: 'bad_input', // already used }); }); test('redeem: expired token → not_found and pruned from the store', async () => { let t = 1700000000000; const { provider, fs } = makeProvider({ now: () => t }); const inv = await provider.createInvite({ role: 'member' }); const token = inv.inviteUrl.split('/').pop(); t += 7 * 24 * 60 * 60 * 1000 + 1; // past TTL await expect(provider.redeemInvite(token)).rejects.toMatchObject({ code: 'not_found' }); const store = JSON.parse(fs.files.get('/data/network-wg-invites.json')); expect(store.invites).toHaveLength(0); }); test('revokeInvite removes the token; unknown id → not_found', async () => { const { provider } = makeProvider(); const inv = await provider.createInvite({ role: 'member' }); await expect(provider.revokeInvite(inv.id)).resolves.toEqual({ revoked: true }); const token = inv.inviteUrl.split('/').pop(); await expect(provider.redeemInvite(token)).rejects.toMatchObject({ code: 'not_found' }); await expect(provider.revokeInvite('nope')).rejects.toMatchObject({ code: 'not_found' }); }); test('createInvite validates role/email (bad_input)', async () => { const { provider } = makeProvider(); await expect(provider.createInvite({})).rejects.toMatchObject({ code: 'bad_input' }); await expect(provider.createInvite({ role: 'member', email: 'nope' })).rejects.toMatchObject({ code: 'bad_input' }); }); }); describe('wireguard provider: unsupported / not-implemented surface', () => { test('syncAccess → not_implemented (DC-151 injects the firewall compiler)', async () => { const { provider } = makeProvider(); await expect(provider.syncAccess()).rejects.toMatchObject({ name: 'ProviderError', code: 'not_implemented', retryable: false, }); }); test('configureDns → unsupported (splitDnsApi capability false)', async () => { const { provider } = makeProvider(); await expect(provider.configureDns({ zone: 'z', server: 's' })).rejects.toMatchObject({ name: 'ProviderError', code: 'unsupported', retryable: false, }); }); }); describe('wireguard provider: registry registration', () => { test('src/network/index registers wireguard alongside tailscale', () => { jest.resetModules(); const { registry } = require('../src/network'); expect(registry.listProviderIds().sort()).toEqual(['tailscale', 'wireguard']); const p = registry.setActiveProvider('wireguard'); expect(p.id()).toBe('wireguard'); // contract validation is enforced by setActiveProvider itself for (const m of CONTRACT_METHODS) expect(typeof p[m]).toBe('function'); registry.reset(); }); });