|
@@ -10,6 +10,7 @@
|
|
|
const { spawn } = require('child_process');
|
|
const { spawn } = require('child_process');
|
|
|
const net = require('net');
|
|
const net = require('net');
|
|
|
const http = require('http');
|
|
const http = require('http');
|
|
|
|
|
+const os = require('os');
|
|
|
const path = require('path');
|
|
const path = require('path');
|
|
|
|
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
@@ -26,11 +27,14 @@ function pickPort() {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function request(port, method, urlPath, body) {
|
|
|
|
|
|
|
+function request(port, method, urlPath, body, host = '127.0.0.1', extraHeaders = {}) {
|
|
|
return new Promise((resolve, reject) => {
|
|
return new Promise((resolve, reject) => {
|
|
|
const req = http.request({
|
|
const req = http.request({
|
|
|
- host: '127.0.0.1', port, method, path: urlPath, timeout: 15000,
|
|
|
|
|
- headers: body !== undefined ? { 'Content-Type': 'application/json' } : {},
|
|
|
|
|
|
|
+ host, port, method, path: urlPath, timeout: 15000,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
|
|
|
+ ...extraHeaders,
|
|
|
|
|
+ },
|
|
|
}, (res) => {
|
|
}, (res) => {
|
|
|
const chunks = [];
|
|
const chunks = [];
|
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('data', (c) => chunks.push(c));
|
|
@@ -48,6 +52,16 @@ function request(port, method, urlPath, body) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 取本机第一个非内部 IPv4 局域网地址(用于访问口令测试;回环地址在服务端直接放行) */
|
|
|
|
|
+function getLanIpv4() {
|
|
|
|
|
+ for (const list of Object.values(os.networkInterfaces())) {
|
|
|
|
|
+ for (const ni of list || []) {
|
|
|
|
|
+ if (ni.family === 'IPv4' && !ni.internal) return ni.address;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const results = [];
|
|
const results = [];
|
|
|
async function check(name, fn) {
|
|
async function check(name, fn) {
|
|
|
try {
|
|
try {
|
|
@@ -74,6 +88,27 @@ async function waitForHealth(port, timeoutMs = 20000) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function waitForHealthLan(lanIp, port, timeoutMs = 20000) {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const start = Date.now();
|
|
|
|
|
+ const probe = () => {
|
|
|
|
|
+ const req = http.request({ host: lanIp, port, path: '/healthz', timeout: 3000 }, (res) => {
|
|
|
|
|
+ res.resume();
|
|
|
|
|
+ if (res.statusCode === 200) return resolve();
|
|
|
|
|
+ retry();
|
|
|
|
|
+ });
|
|
|
|
|
+ req.on('error', retry);
|
|
|
|
|
+ req.on('timeout', () => { req.destroy(); retry(); });
|
|
|
|
|
+ req.end();
|
|
|
|
|
+ };
|
|
|
|
|
+ const retry = () => {
|
|
|
|
|
+ if (Date.now() - start > timeoutMs) return reject(new Error('服务未在超时内启动(LAN)'));
|
|
|
|
|
+ setTimeout(probe, 300);
|
|
|
|
|
+ };
|
|
|
|
|
+ probe();
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function enc(s) { return encodeURIComponent(s); }
|
|
function enc(s) { return encodeURIComponent(s); }
|
|
|
|
|
|
|
|
async function main() {
|
|
async function main() {
|
|
@@ -276,6 +311,48 @@ async function main() {
|
|
|
});
|
|
});
|
|
|
expect(exited, 'SIGTERM 后 8 秒内未退出');
|
|
expect(exited, 'SIGTERM 后 8 秒内未退出');
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 访问口令(需局域网 IP:本机回环地址在守卫中直接放行,测不到 401) ----
|
|
|
|
|
+ const lanIp = getLanIpv4();
|
|
|
|
|
+ if (!lanIp) {
|
|
|
|
|
+ results.push({ name: '访问口令(无局域网 IP,跳过)', ok: true });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const tport = await pickPort();
|
|
|
|
|
+ const tchild = spawn(process.execPath, [path.join(ROOT, 'server.js')], {
|
|
|
|
|
+ env: { ...process.env, PORT: String(tport), HOST: '0.0.0.0', ACCESS_TOKEN: 'smoke-token' },
|
|
|
|
|
+ stdio: ['ignore', 'ignore', 'ignore'],
|
|
|
|
|
+ });
|
|
|
|
|
+ try {
|
|
|
|
|
+ await waitForHealthLan(lanIp, tport);
|
|
|
|
|
+ await check('口令开启:无口令访问 API → 401', async () => {
|
|
|
|
|
+ const r = await request(tport, 'GET', '/api/models', undefined, lanIp);
|
|
|
|
|
+ expect(r.status === 401, `status=${r.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ await check('口令开启:?token= 通过', async () => {
|
|
|
|
|
+ const r = await request(tport, 'GET', `/api/models?token=smoke-token`, undefined, lanIp);
|
|
|
|
|
+ expect(r.status === 200, `status=${r.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ await check('口令开启:X-Access-Token 头通过', async () => {
|
|
|
|
|
+ const r = await request(tport, 'GET', '/api/models', undefined, lanIp, { 'X-Access-Token': 'smoke-token' });
|
|
|
|
|
+ expect(r.status === 200, `status=${r.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ await check('口令开启:cookie 通过', async () => {
|
|
|
|
|
+ const r = await request(tport, 'GET', '/api/models', undefined, lanIp, { Cookie: 'accessToken=smoke-token' });
|
|
|
|
|
+ expect(r.status === 200, `status=${r.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ await check('口令开启:/healthz 与静态页放行', async () => {
|
|
|
|
|
+ const h = await request(tport, 'GET', '/healthz', undefined, lanIp);
|
|
|
|
|
+ const s = await request(tport, 'GET', '/', undefined, lanIp);
|
|
|
|
|
+ expect(h.status === 200 && s.status === 200, `healthz=${h.status} static=${s.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ await check('口令开启:错误口令 → 401', async () => {
|
|
|
|
|
+ const r = await request(tport, 'GET', '/api/models?token=wrong', undefined, lanIp);
|
|
|
|
|
+ expect(r.status === 401, `status=${r.status}`);
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ try { tchild.kill('SIGKILL'); } catch { /* ignore */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
results.push({ name: '(测试执行中断)', ok: false, error: e.message });
|
|
results.push({ name: '(测试执行中断)', ok: false, error: e.message });
|
|
|
} finally {
|
|
} finally {
|