59 lines
4.4 KiB
JavaScript
59 lines
4.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { after, before, test } from 'node:test';
|
|
import { spawn } from 'node:child_process';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'stunmap-test-'));
|
|
const port = 17991;
|
|
const auth = `Basic ${Buffer.from('admin:test-password').toString('base64')}`;
|
|
let app;
|
|
|
|
async function request(url, options = {}) {
|
|
return fetch(`http://127.0.0.1:${port}${url}`, { ...options, headers: { authorization: auth, 'content-type': 'application/json', ...(options.headers || {}) } });
|
|
}
|
|
|
|
before(async () => {
|
|
app = spawn(process.execPath, ['server.js'], { env: { ...process.env, PORT: String(port), STUNMAP_DATA_DIR: directory, NATMAP_BIN: '/does/not/exist', STUNMAP_ADMIN_PASSWORD: 'test-password' } });
|
|
await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('server did not start')), 3000); app.stdout.on('data', () => { clearTimeout(timer); resolve(); }); app.on('error', reject); });
|
|
});
|
|
after(async () => { app.kill('SIGTERM'); await fs.rm(directory, { recursive: true, force: true }); });
|
|
|
|
test('requires HTTP basic authentication', async () => {
|
|
const response = await fetch(`http://127.0.0.1:${port}/api/rules`);
|
|
assert.equal(response.status, 401);
|
|
});
|
|
|
|
test('creates, validates, and stops a Lucky-style STUN rule', async () => {
|
|
const body = { name: 'web', enabled: false, protocol: 'tcp', bindPort: 0, stunServer: 'turn.cloudflare.com:3478', keepaliveServer: 'www.cloudflare.com:80', interval: 30, checkEvery: 10, mode: 'forward', targetHost: '192.168.1.20', targetPort: 443, routerMapping: 'upnp', firewallNote: 'WAN allowed' };
|
|
const created = await request('/api/rules', { method: 'POST', body: JSON.stringify(body) });
|
|
assert.equal(created.status, 201); const rule = (await created.json()).rule;
|
|
assert.equal(rule.protocol, 'tcp'); assert.equal(rule.mode, 'forward'); assert.equal(rule.routerMapping, 'upnp');
|
|
const list = await request('/api/rules'); const rules = (await list.json()).rules;
|
|
assert.equal(rules.length, 1); assert.equal(rules[0].running, false);
|
|
const invalid = await request('/api/rules', { method: 'POST', body: JSON.stringify({ ...body, name: 'bad', protocol: 'udp', mode: 'forward', targetPort: 0 }) });
|
|
assert.equal(invalid.status, 400);
|
|
const udpBind = await request('/api/rules', { method: 'POST', body: JSON.stringify({ ...body, name: 'udp-bind', protocol: 'udp', mode: 'bind', targetHost: '', targetPort: 0 }) });
|
|
assert.equal(udpBind.status, 400);
|
|
const incompleteProbe = await request('/api/rules', { method: 'POST', body: JSON.stringify({ ...body, name: 'incomplete-probe', externalProbeUrl: 'https://probe.example.com/probe' }) });
|
|
assert.equal(incompleteProbe.status, 400);
|
|
const webhook = await request('/api/rules', { method: 'POST', body: JSON.stringify({ ...body, name: 'webhook-template', webhookUrl: 'https://hooks.example.com/{{STUN_PUBLIC_ADDR}}', webhookMethod: 'post', webhookBody: '{"ip":"{{STUN_PUBLIC_IP}}","port":{{STUN_PUBLIC_PORT}},"name":"{{STUN_RULE_NAME}}"}' }) });
|
|
assert.equal(webhook.status, 201); const webhookRule = (await webhook.json()).rule;
|
|
assert.equal(webhookRule.webhookBody, '{"ip":"{{STUN_PUBLIC_IP}}","port":{{STUN_PUBLIC_PORT}},"name":"{{STUN_RULE_NAME}}"}');
|
|
const invalidWebhookBody = await request('/api/rules', { method: 'POST', body: JSON.stringify({ ...body, name: 'invalid-webhook-template', webhookUrl: 'https://hooks.example.com', webhookMethod: 'post', webhookBody: '{"ip":"{{MISSING_VARIABLE}}"}' }) });
|
|
assert.equal(invalidWebhookBody.status, 400);
|
|
const removed = await request(`/api/rules/${rule.id}`, { method: 'DELETE' });
|
|
assert.equal(removed.status, 204);
|
|
});
|
|
|
|
test('keeps the console available when an enabled mapping engine cannot start', async () => {
|
|
const body = { name: 'engine-error', enabled: true, protocol: 'udp', bindPort: 0, stunServer: 'turn.cloudflare.com:3478', keepaliveServer: '', interval: 30, checkEvery: 10, mode: 'forward', targetHost: '192.168.1.20', targetPort: 51820, routerMapping: 'none' };
|
|
const created = await request('/api/rules', { method: 'POST', body: JSON.stringify(body) });
|
|
assert.equal(created.status, 201);
|
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
const rules = (await (await request('/api/rules')).json()).rules;
|
|
const rule = rules.find((item) => item.name === 'engine-error');
|
|
assert.ok(rule.logs.some((entry) => entry.level === 'error'));
|
|
});
|