refactor: modularize backend security and observability
This commit is contained in:
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);
|
||||
Reference in New Issue
Block a user