181 lines
7.1 KiB
JavaScript
181 lines
7.1 KiB
JavaScript
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);
|