refactor: modularize backend security and observability
This commit is contained in:
113
backend/tests/architecture-tests.js
Normal file
113
backend/tests/architecture-tests.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const projectRoot = path.resolve(__dirname, '..', '..');
|
||||
const serverPath = path.join(projectRoot, 'backend', 'server.js');
|
||||
const source = fs.readFileSync(serverPath, 'utf8');
|
||||
const results = { passed: 0, failed: 0 };
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
results.passed += 1;
|
||||
console.log(` [PASS] ${name}`);
|
||||
} catch (error) {
|
||||
results.failed += 1;
|
||||
console.error(` [FAIL] ${name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n========== 架构约束测试 ==========\n');
|
||||
|
||||
test('Express async 错误补丁在所有路由之前加载', () => {
|
||||
const patchIndex = source.indexOf("require('express-async-errors')");
|
||||
const routeIndex = source.search(/\bapp\.(get|post|put|delete|patch)\s*\(/);
|
||||
assert.ok(patchIndex >= 0);
|
||||
assert.ok(routeIndex > patchIndex);
|
||||
});
|
||||
|
||||
test('全局错误中间件保持为最后一个 app.use', () => {
|
||||
const errorHandlerIndex = source.indexOf('app.use(expressErrorHandler);');
|
||||
assert.ok(errorHandlerIndex >= 0);
|
||||
assert.strictEqual(source.lastIndexOf('app.use('), errorHandlerIndex);
|
||||
});
|
||||
|
||||
test('抽取的纯函数不在 server.js 保留第二份定义', () => {
|
||||
const extractedFunctions = [
|
||||
'normalizeVersion',
|
||||
'normalizeSha256',
|
||||
'normalizeVirtualPath',
|
||||
'buildHttpDownloadUrl',
|
||||
'parseDownloadTrafficLine',
|
||||
'isShareIpAllowed',
|
||||
'resolveClientType',
|
||||
'parseBooleanLike'
|
||||
];
|
||||
for (const functionName of extractedFunctions) {
|
||||
assert.strictEqual(new RegExp(`function\\s+${functionName}\\s*\\(`).test(source), false, functionName);
|
||||
}
|
||||
});
|
||||
|
||||
test('旧占位路由已清除且真实系统路由已挂载', () => {
|
||||
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'health.js')), false);
|
||||
assert.strictEqual(fs.existsSync(path.join(projectRoot, 'backend', 'routes', 'index.js')), false);
|
||||
assert.ok(source.includes("require('./routes/system.routes')"));
|
||||
assert.ok(source.includes("app.use('/api', createSystemRouter"));
|
||||
});
|
||||
|
||||
test('package.json 与 package-lock.json 顶层依赖同步', () => {
|
||||
const packageJson = require('../package.json');
|
||||
const packageLock = require('../package-lock.json');
|
||||
assert.deepStrictEqual(packageLock.packages[''].dependencies, packageJson.dependencies);
|
||||
});
|
||||
|
||||
test('前端 package.json 与 lock 文件依赖同步且没有手工缓存版本号', () => {
|
||||
const frontendPackage = require('../../frontend/package.json');
|
||||
const frontendLock = require('../../frontend/package-lock.json');
|
||||
assert.deepStrictEqual(frontendLock.packages[''].dependencies, frontendPackage.dependencies);
|
||||
assert.deepStrictEqual(frontendLock.packages[''].devDependencies, frontendPackage.devDependencies);
|
||||
|
||||
const frontendDirectory = path.join(projectRoot, 'frontend');
|
||||
for (const fileName of fs.readdirSync(frontendDirectory).filter((name) => name.endsWith('.html'))) {
|
||||
const html = fs.readFileSync(path.join(frontendDirectory, fileName), 'utf8');
|
||||
assert.strictEqual(/\?v=\d+/.test(html), false, fileName);
|
||||
}
|
||||
});
|
||||
|
||||
test('生产部署统一使用 Vite dist 且不会删除 lock 文件', () => {
|
||||
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||
const nginxConfig = fs.readFileSync(path.join(projectRoot, 'nginx', 'nginx.conf'), 'utf8');
|
||||
assert.ok(installer.includes('npm ci --include=dev'));
|
||||
assert.ok(installer.includes('npm run build'));
|
||||
assert.ok(installer.includes('frontend/dist'));
|
||||
assert.strictEqual(/rm\s+-rf\s+node_modules\s+package-lock\.json/.test(installer), false);
|
||||
assert.ok(nginxConfig.includes('root /usr/share/nginx/html;'));
|
||||
assert.ok(nginxConfig.includes('alias /runtime/downloads/;'));
|
||||
});
|
||||
|
||||
test('install.sh 生成的 Nginx server 块花括号平衡', () => {
|
||||
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||
const templates = [...installer.matchAll(/cat > .*?<< EOF\r?\n([\s\S]*?)\r?\nEOF/g)]
|
||||
.map((match) => match[1])
|
||||
.filter((template) => template.includes('server {'));
|
||||
assert.strictEqual(templates.length, 3);
|
||||
for (const template of templates) {
|
||||
const openingBraces = (template.match(/\{/g) || []).length;
|
||||
const closingBraces = (template.match(/\}/g) || []).length;
|
||||
assert.strictEqual(openingBraces, closingBraces);
|
||||
}
|
||||
});
|
||||
|
||||
test('PM2 部署没有启用 cluster 或多实例参数', () => {
|
||||
const installer = fs.readFileSync(path.join(projectRoot, 'install.sh'), 'utf8');
|
||||
assert.strictEqual(/pm2\s+start[^\n]*(?:\s-i\s|--instances)/.test(installer), false);
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('测试总结');
|
||||
console.log('========================================');
|
||||
console.log(`通过: ${results.passed}`);
|
||||
console.log(`失败: ${results.failed}`);
|
||||
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
155
backend/tests/domain-utils-tests.js
Normal file
155
backend/tests/domain-utils-tests.js
Normal file
@@ -0,0 +1,155 @@
|
||||
const assert = require('assert');
|
||||
|
||||
const { normalizeNonNegativeInteger, parseBooleanLike } = require('../utils/coerce');
|
||||
const {
|
||||
buildDeviceName,
|
||||
detectDeviceTypeFromUserAgent,
|
||||
inferPlatformFromUserAgent,
|
||||
normalizeClientIp,
|
||||
normalizeClientType,
|
||||
resolveClientType,
|
||||
sanitizeDeviceText
|
||||
} = require('../utils/device');
|
||||
const { parseDownloadTrafficLine, parseDownloadTrafficLogTime } = require('../utils/oss-log-parser');
|
||||
const { isPathInside, normalizeVirtualPath } = require('../utils/path-safety');
|
||||
const { isPathWithinShare, isShareIpAllowed, isValidShareCode, parseShareIpWhitelist } = require('../utils/share');
|
||||
const { buildHttpDownloadUrl, sanitizeHttpBaseUrl } = require('../utils/url');
|
||||
const {
|
||||
compareLooseVersion,
|
||||
normalizeReleaseNotes,
|
||||
normalizeSha256,
|
||||
normalizeVersion
|
||||
} = require('../utils/version');
|
||||
|
||||
const results = { passed: 0, failed: 0 };
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
results.passed += 1;
|
||||
console.log(` [PASS] ${name}`);
|
||||
} catch (error) {
|
||||
results.failed += 1;
|
||||
console.error(` [FAIL] ${name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n========== 领域纯函数测试 ==========\n');
|
||||
|
||||
test('版本号规范化与宽松比较', () => {
|
||||
assert.strictEqual(normalizeVersion(' v1.2.3 '), 'v1.2.3');
|
||||
assert.strictEqual(normalizeVersion('', '1.0.0'), '1.0.0');
|
||||
assert.strictEqual(compareLooseVersion('v1.10.0', '1.9.9'), 1);
|
||||
assert.strictEqual(compareLooseVersion('1.2', '1.2.0'), 0);
|
||||
assert.strictEqual(compareLooseVersion('1.2.0', '2.0.0'), -1);
|
||||
});
|
||||
|
||||
test('发布说明转义换行被还原', () => {
|
||||
assert.strictEqual(normalizeReleaseNotes('first\\r\\nsecond\\n'), 'first\nsecond');
|
||||
});
|
||||
|
||||
test('SHA256 只接受完整十六进制摘要', () => {
|
||||
assert.strictEqual(normalizeSha256(' A'.repeat(64)), '');
|
||||
assert.strictEqual(normalizeSha256('A'.repeat(64)), 'a'.repeat(64));
|
||||
assert.strictEqual(normalizeSha256('a'.repeat(63)), '');
|
||||
});
|
||||
|
||||
test('非负整数与布尔配置规范化', () => {
|
||||
assert.strictEqual(normalizeNonNegativeInteger(12.9), 12);
|
||||
assert.strictEqual(normalizeNonNegativeInteger(-1, 4), 4);
|
||||
assert.strictEqual(parseBooleanLike('YES'), true);
|
||||
assert.strictEqual(parseBooleanLike('off', true), false);
|
||||
assert.strictEqual(parseBooleanLike('invalid', true), true);
|
||||
});
|
||||
|
||||
test('物理路径边界不接受兄弟目录', () => {
|
||||
const parent = require('path').resolve('/tmp/root');
|
||||
assert.strictEqual(isPathInside(parent, require('path').join(parent, 'child')), true);
|
||||
assert.strictEqual(isPathInside(parent, require('path').resolve('/tmp/root-other')), false);
|
||||
});
|
||||
|
||||
test('虚拟路径统一分隔符并拒绝遍历和空字节', () => {
|
||||
assert.strictEqual(normalizeVirtualPath('folder\\file.txt'), '/folder/file.txt');
|
||||
assert.strictEqual(normalizeVirtualPath('/folder/../secret'), null);
|
||||
assert.strictEqual(normalizeVirtualPath('/folder/%2e%2e/secret'), null);
|
||||
assert.strictEqual(normalizeVirtualPath('/folder/%2500.txt'), null);
|
||||
assert.strictEqual(normalizeVirtualPath('/'), '/');
|
||||
});
|
||||
|
||||
test('HTTP 基础地址拒绝危险协议并移除查询参数', () => {
|
||||
assert.strictEqual(sanitizeHttpBaseUrl('javascript:alert(1)'), null);
|
||||
assert.strictEqual(sanitizeHttpBaseUrl('https://example.test/base/?token=x#hash'), 'https://example.test/base');
|
||||
});
|
||||
|
||||
test('下载地址逐段编码且不继承查询参数', () => {
|
||||
assert.strictEqual(
|
||||
buildHttpDownloadUrl('https://example.test/base?token=x', '/目录/a b.txt'),
|
||||
'https://example.test/base/%E7%9B%AE%E5%BD%95/a%20b.txt'
|
||||
);
|
||||
});
|
||||
|
||||
test('OSS 日志时间正确应用时区', () => {
|
||||
const date = parseDownloadTrafficLogTime('[27/Jul/2026:12:34:56 +0800]');
|
||||
assert.strictEqual(date.toISOString(), '2026-07-27T04:34:56.000Z');
|
||||
assert.strictEqual(parseDownloadTrafficLogTime('[31/Feb/2026:12:00:00 +0800]'), null);
|
||||
assert.strictEqual(parseDownloadTrafficLogTime('[27/Jul/2026:12:00:00 +2460]'), null);
|
||||
});
|
||||
|
||||
test('OSS 下载日志解析用户、对象键、字节和时间', () => {
|
||||
const line = '1.2.3.4 - - [27/Jul/2026:12:34:56 +0800] "GET /user_42/folder/a%20b.txt?signature=x HTTP/1.1" 206 1024 "-" "ua"';
|
||||
const result = parseDownloadTrafficLine(line);
|
||||
assert.strictEqual(result.userId, 42);
|
||||
assert.strictEqual(result.bytes, 1024);
|
||||
assert.strictEqual(result.objectKey, 'user_42/folder/a b.txt');
|
||||
assert.strictEqual(result.eventAt.toISOString(), '2026-07-27T04:34:56.000Z');
|
||||
});
|
||||
|
||||
test('OSS 日志只统计成功 GET 下载', () => {
|
||||
const fallback = new Date('2026-01-01T00:00:00Z');
|
||||
assert.strictEqual(parseDownloadTrafficLine('"HEAD /user_1/a HTTP/1.1" 200 10', fallback), null);
|
||||
assert.strictEqual(parseDownloadTrafficLine('"GET /user_1/a HTTP/1.1" 404 10', fallback), null);
|
||||
assert.strictEqual(parseDownloadTrafficLine('"GET /public/a HTTP/1.1" 200 10', fallback), null);
|
||||
assert.strictEqual(parseDownloadTrafficLine('"GET /user_1/a HTTP/1.1" 200 -', fallback), null);
|
||||
});
|
||||
|
||||
test('分享码与分享路径严格限定', () => {
|
||||
assert.strictEqual(isValidShareCode('Abc123'), true);
|
||||
assert.strictEqual(isValidShareCode('../bad'), false);
|
||||
assert.strictEqual(isPathWithinShare('/folder/a.txt', { share_type: 'folder', share_path: '/folder' }), true);
|
||||
assert.strictEqual(isPathWithinShare('/folder-other/a.txt', { share_type: 'folder', share_path: '/folder' }), false);
|
||||
assert.strictEqual(isPathWithinShare('/folder/a.txt', { share_type: 'file', share_path: '/folder/a.txt' }), true);
|
||||
assert.strictEqual(isPathWithinShare('/folder/b.txt', { share_type: 'file', share_path: '/folder/a.txt' }), false);
|
||||
});
|
||||
|
||||
test('IP 白名单支持分隔符、精确匹配与前缀规则', () => {
|
||||
const whitelist = parseShareIpWhitelist('127.0.0.1, 10.0.*;192.168.1.5');
|
||||
assert.deepStrictEqual(whitelist, ['127.0.0.1', '10.0.*', '192.168.1.5']);
|
||||
assert.strictEqual(isShareIpAllowed('10.0.2.3', whitelist), true);
|
||||
assert.strictEqual(isShareIpAllowed('192.168.1.6', whitelist), false);
|
||||
assert.strictEqual(isShareIpAllowed('', whitelist), false);
|
||||
assert.strictEqual(isShareIpAllowed('', []), true);
|
||||
});
|
||||
|
||||
test('客户端 IP 与设备类型被稳定规范化', () => {
|
||||
assert.strictEqual(normalizeClientIp('::ffff:127.0.0.1'), '127.0.0.1');
|
||||
assert.strictEqual(normalizeClientIp('::1'), '127.0.0.1');
|
||||
assert.strictEqual(detectDeviceTypeFromUserAgent('Mozilla/5.0 iPhone'), 'mobile');
|
||||
assert.strictEqual(inferPlatformFromUserAgent('Mozilla/5.0 (Windows NT 10.0)'), 'Windows');
|
||||
assert.strictEqual(normalizeClientType(' DESKTOP '), 'desktop');
|
||||
assert.strictEqual(normalizeClientType('bot'), '');
|
||||
assert.strictEqual(resolveClientType('', 'wanwan-cloud-desktop/1.0'), 'desktop');
|
||||
});
|
||||
|
||||
test('设备名称限制长度并按客户端类型回退', () => {
|
||||
assert.strictEqual(sanitizeDeviceText(' abc ', 2), 'ab');
|
||||
assert.strictEqual(buildDeviceName({ clientType: 'mobile', platform: 'iOS' }), '移动端浏览器 · iOS');
|
||||
assert.strictEqual(buildDeviceName({ clientType: 'web', deviceName: ' 我的设备 ', platform: 'Windows' }), '我的设备');
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('测试总结');
|
||||
console.log('========================================');
|
||||
console.log(`通过: ${results.passed}`);
|
||||
console.log(`失败: ${results.failed}`);
|
||||
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
180
backend/tests/middleware-tests.js
Normal file
180
backend/tests/middleware-tests.js
Normal file
@@ -0,0 +1,180 @@
|
||||
process.env.LOG_LEVEL = 'silent';
|
||||
|
||||
const assert = require('assert');
|
||||
const { EventEmitter } = require('events');
|
||||
const fs = require('fs');
|
||||
|
||||
const {
|
||||
createCsrfCookieMiddleware,
|
||||
csrfProtection,
|
||||
generateCsrfToken,
|
||||
tokensEqual
|
||||
} = require('../middleware/csrf');
|
||||
const { RateLimiter } = require('../middleware/rate-limit');
|
||||
const { applySecurityHeaders, createHttpsEnforcementMiddleware } = require('../middleware/security');
|
||||
const { expressErrorHandler } = require('../middleware/error-handler');
|
||||
const { requestContextMiddleware, redactLogValue } = require('../utils/logger');
|
||||
const { createHealthService, normalizeMinimumFreeBytes } = require('../services/health-service');
|
||||
|
||||
const results = { passed: 0, failed: 0 };
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
results.passed += 1;
|
||||
console.log(` [PASS] ${name}`);
|
||||
} catch (error) {
|
||||
results.failed += 1;
|
||||
console.error(` [FAIL] ${name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createResponse() {
|
||||
const response = new EventEmitter();
|
||||
response.headers = {};
|
||||
response.statusCode = 200;
|
||||
response.payload = null;
|
||||
response.cookies = [];
|
||||
response.setHeader = (name, value) => { response.headers[name] = value; };
|
||||
response.removeHeader = (name) => { delete response.headers[name]; };
|
||||
response.status = (code) => { response.statusCode = code; return response; };
|
||||
response.json = (payload) => { response.payload = payload; return response; };
|
||||
response.cookie = (name, value, options) => { response.cookies.push({ name, value, options }); };
|
||||
return response;
|
||||
}
|
||||
|
||||
console.log('\n========== 中间件与可观测性测试 ==========\n');
|
||||
|
||||
test('安全响应头覆盖 CSP、HSTS、Referrer 与 Permissions Policy', () => {
|
||||
const response = createResponse();
|
||||
applySecurityHeaders({ secure: true }, response);
|
||||
assert.ok(response.headers['Content-Security-Policy'].includes("object-src 'none'"));
|
||||
assert.ok(response.headers['Strict-Transport-Security']);
|
||||
assert.strictEqual(response.headers['X-Content-Type-Options'], 'nosniff');
|
||||
assert.strictEqual(response.headers['Referrer-Policy'], 'strict-origin-when-cross-origin');
|
||||
assert.ok(response.headers['Permissions-Policy']);
|
||||
});
|
||||
|
||||
test('HTTPS 中间件阻止不安全请求但放行本机健康检查', () => {
|
||||
const middleware = createHttpsEnforcementMiddleware({ enabled: true });
|
||||
const blockedResponse = createResponse();
|
||||
middleware({ path: '/api/config', secure: false, socket: { remoteAddress: '10.0.0.1' }, id: 'req-1' }, blockedResponse, () => assert.fail('不应放行'));
|
||||
assert.strictEqual(blockedResponse.statusCode, 400);
|
||||
assert.strictEqual(blockedResponse.payload.requestId, 'req-1');
|
||||
|
||||
let nextCalled = false;
|
||||
middleware({ path: '/api/health', secure: false, socket: { remoteAddress: '127.0.0.1' } }, createResponse(), () => { nextCalled = true; });
|
||||
assert.strictEqual(nextCalled, true);
|
||||
});
|
||||
|
||||
test('请求上下文接受安全 requestId 并回写响应头', () => {
|
||||
const request = { headers: { 'x-request-id': 'trace-123' }, method: 'GET', path: '/api/config' };
|
||||
const response = createResponse();
|
||||
requestContextMiddleware(request, response, () => {});
|
||||
assert.strictEqual(request.id, 'trace-123');
|
||||
assert.strictEqual(response.headers['X-Request-ID'], 'trace-123');
|
||||
response.emit('finish');
|
||||
});
|
||||
|
||||
test('非法 requestId 被替换,日志敏感字段被脱敏', () => {
|
||||
const request = { headers: { 'x-request-id': 'bad id\n' }, method: 'GET', path: '/' };
|
||||
const response = createResponse();
|
||||
requestContextMiddleware(request, response, () => {});
|
||||
assert.notStrictEqual(request.id, 'bad id\n');
|
||||
assert.match(request.id, /^[0-9a-f-]{36}$/);
|
||||
const redacted = redactLogValue({ password: 'secret', note: 'token=abc', nested: { authorization: 'Bearer value' } });
|
||||
assert.strictEqual(redacted.password, '[REDACTED]');
|
||||
assert.strictEqual(redacted.note, 'token=[REDACTED]');
|
||||
assert.strictEqual(redacted.nested.authorization, '[REDACTED]');
|
||||
response.emit('finish');
|
||||
});
|
||||
|
||||
test('CSRF token 使用固定长度并支持常量时间比较', () => {
|
||||
const token = generateCsrfToken();
|
||||
assert.match(token, /^[a-f0-9]{64}$/);
|
||||
assert.strictEqual(tokensEqual(token, token), true);
|
||||
assert.strictEqual(tokensEqual(token, `${token}0`), false);
|
||||
});
|
||||
|
||||
test('CSRF Cookie 中间件只在缺少 token 时设置 Cookie', () => {
|
||||
const middleware = createCsrfCookieMiddleware({ secureCookies: true });
|
||||
const response = createResponse();
|
||||
middleware({ cookies: {} }, response, () => {});
|
||||
assert.strictEqual(response.cookies.length, 1);
|
||||
assert.strictEqual(response.cookies[0].options.secure, true);
|
||||
assert.strictEqual(response.cookies[0].options.sameSite, 'strict');
|
||||
});
|
||||
|
||||
test('Cookie 会话的写请求必须通过 CSRF 校验', () => {
|
||||
const response = createResponse();
|
||||
csrfProtection({
|
||||
method: 'POST',
|
||||
path: '/api/example',
|
||||
id: 'req-2',
|
||||
cookies: { token: 'session', csrf_token: 'a' },
|
||||
headers: { 'x-csrf-token': 'b' }
|
||||
}, response, () => assert.fail('不应放行'));
|
||||
assert.strictEqual(response.statusCode, 403);
|
||||
assert.strictEqual(response.payload.requestId, 'req-2');
|
||||
});
|
||||
|
||||
test('失败限流器达到阈值后封锁并可在成功后清除', () => {
|
||||
const limiter = new RateLimiter({ maxAttempts: 2, windowMs: 1000, blockDuration: 1000 });
|
||||
try {
|
||||
assert.strictEqual(limiter.recordFailure('key').blocked, false);
|
||||
assert.strictEqual(limiter.recordFailure('key').blocked, true);
|
||||
assert.strictEqual(limiter.isBlocked('key'), true);
|
||||
limiter.recordSuccess('key');
|
||||
assert.strictEqual(limiter.isBlocked('key'), false);
|
||||
} finally {
|
||||
limiter.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test('健康检查同时验证数据库与磁盘', () => {
|
||||
const service = createHealthService({
|
||||
db: { prepare: () => ({ get: () => ({ ok: 1 }) }) },
|
||||
diskPath: __dirname,
|
||||
minimumFreeBytes: 0
|
||||
});
|
||||
const result = service.check();
|
||||
assert.strictEqual(result.healthy, true);
|
||||
assert.strictEqual(result.checks.database.status, 'ok');
|
||||
assert.strictEqual(result.checks.disk.status, 'ok');
|
||||
assert.ok(result.checks.disk.freeBytes > 0);
|
||||
assert.strictEqual(fs.existsSync(__dirname), true);
|
||||
assert.strictEqual(normalizeMinimumFreeBytes(-1), 256 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test('数据库异常会让健康状态降级', () => {
|
||||
const service = createHealthService({
|
||||
db: { prepare: () => { throw Object.assign(new Error('down'), { code: 'SQLITE_DOWN' }); } },
|
||||
diskPath: __dirname,
|
||||
minimumFreeBytes: 0
|
||||
});
|
||||
const result = service.check();
|
||||
assert.strictEqual(result.healthy, false);
|
||||
assert.strictEqual(result.checks.database.status, 'error');
|
||||
});
|
||||
|
||||
test('错误响应携带 requestId 并隐藏服务端异常', () => {
|
||||
const response = createResponse();
|
||||
expressErrorHandler(new Error('secret detail'), {
|
||||
id: 'req-3',
|
||||
method: 'GET',
|
||||
path: '/api/example'
|
||||
}, response, () => assert.fail('不应继续'));
|
||||
assert.deepStrictEqual(response.payload, {
|
||||
success: false,
|
||||
message: '服务器内部错误',
|
||||
requestId: 'req-3'
|
||||
});
|
||||
});
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('测试总结');
|
||||
console.log('========================================');
|
||||
console.log(`通过: ${results.passed}`);
|
||||
console.log(`失败: ${results.failed}`);
|
||||
|
||||
process.exit(results.failed > 0 ? 1 : 0);
|
||||
@@ -1,3 +1,5 @@
|
||||
process.env.LOG_LEVEL = 'silent';
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
parseDateTimeValue,
|
||||
|
||||
@@ -8,6 +8,9 @@ const path = require('path');
|
||||
const testFiles = [
|
||||
'boundary-tests.js',
|
||||
'production-utils-tests.js',
|
||||
'domain-utils-tests.js',
|
||||
'middleware-tests.js',
|
||||
'architecture-tests.js',
|
||||
'archive-tests.js',
|
||||
'network-concurrent-tests.js',
|
||||
'state-consistency-tests.js'
|
||||
|
||||
Reference in New Issue
Block a user