247 lines
8.2 KiB
JavaScript
247 lines
8.2 KiB
JavaScript
process.env.LOG_LEVEL = 'silent';
|
|
|
|
const assert = require('assert');
|
|
const {
|
|
parseDateTimeValue,
|
|
formatDateTimeForSqlite,
|
|
getDateKeyFromDate,
|
|
getRecentDateKeys,
|
|
getNextDownloadResetTime,
|
|
normalizeTimeHHmm,
|
|
isCurrentTimeInWindow
|
|
} = require('../utils/datetime');
|
|
const {
|
|
MAX_DOWNLOAD_TRAFFIC_BYTES,
|
|
normalizeDownloadTrafficQuota,
|
|
normalizeDownloadTrafficUsed,
|
|
getDownloadTrafficState,
|
|
resolveDownloadTrafficPolicyUpdates
|
|
} = require('../utils/download-quota');
|
|
const { expressErrorHandler } = require('../middleware/error-handler');
|
|
|
|
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(headersSent = false) {
|
|
return {
|
|
headersSent,
|
|
statusCode: null,
|
|
payload: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(payload) {
|
|
this.payload = payload;
|
|
return this;
|
|
}
|
|
};
|
|
}
|
|
|
|
console.log('\n========== 生产工具模块测试 ==========\n');
|
|
|
|
test('SQLite 日期时间可以被解析', () => {
|
|
const parsed = parseDateTimeValue('2026-07-27 12:34:56');
|
|
assert.ok(parsed instanceof Date);
|
|
assert.strictEqual(parsed.getFullYear(), 2026);
|
|
assert.strictEqual(parsed.getMonth(), 6);
|
|
assert.strictEqual(parsed.getDate(), 27);
|
|
});
|
|
|
|
test('非法日期时间返回 null', () => {
|
|
assert.strictEqual(parseDateTimeValue('not-a-date'), null);
|
|
assert.strictEqual(parseDateTimeValue(null), null);
|
|
});
|
|
|
|
test('日期时间按 SQLite 格式输出', () => {
|
|
const value = new Date(2026, 6, 27, 3, 4, 5);
|
|
assert.strictEqual(formatDateTimeForSqlite(value), '2026-07-27 03:04:05');
|
|
});
|
|
|
|
test('日期键校验有效和无效日期', () => {
|
|
assert.strictEqual(getDateKeyFromDate(new Date(2026, 0, 2)), '2026-01-02');
|
|
assert.strictEqual(getDateKeyFromDate('invalid'), null);
|
|
});
|
|
|
|
test('最近日期键按时间顺序生成', () => {
|
|
const keys = getRecentDateKeys(3, new Date(2026, 0, 2, 12, 0, 0));
|
|
assert.deepStrictEqual(keys, ['2025-12-31', '2026-01-01', '2026-01-02']);
|
|
});
|
|
|
|
test('下载配额重置时间支持日周月周期', () => {
|
|
assert.strictEqual(
|
|
formatDateTimeForSqlite(getNextDownloadResetTime('2026-01-01 00:00:00', 'daily')),
|
|
'2026-01-02 00:00:00'
|
|
);
|
|
assert.strictEqual(
|
|
formatDateTimeForSqlite(getNextDownloadResetTime('2026-01-01 00:00:00', 'weekly')),
|
|
'2026-01-08 00:00:00'
|
|
);
|
|
assert.strictEqual(
|
|
formatDateTimeForSqlite(getNextDownloadResetTime('2026-01-01 00:00:00', 'monthly')),
|
|
'2026-02-01 00:00:00'
|
|
);
|
|
assert.strictEqual(getNextDownloadResetTime('2026-01-01 00:00:00', 'none'), null);
|
|
});
|
|
|
|
test('访问时间格式被严格规范化', () => {
|
|
assert.strictEqual(normalizeTimeHHmm(' 09:05 '), '09:05');
|
|
assert.strictEqual(normalizeTimeHHmm('9:05'), null);
|
|
assert.strictEqual(normalizeTimeHHmm('24:00'), null);
|
|
});
|
|
|
|
test('同日访问时间窗口正确判断', () => {
|
|
assert.strictEqual(isCurrentTimeInWindow('09:00', '18:00', new Date(2026, 0, 1, 12, 0)), true);
|
|
assert.strictEqual(isCurrentTimeInWindow('09:00', '18:00', new Date(2026, 0, 1, 18, 0)), false);
|
|
});
|
|
|
|
test('跨日访问时间窗口正确判断', () => {
|
|
assert.strictEqual(isCurrentTimeInWindow('22:00', '06:00', new Date(2026, 0, 1, 23, 0)), true);
|
|
assert.strictEqual(isCurrentTimeInWindow('22:00', '06:00', new Date(2026, 0, 1, 12, 0)), false);
|
|
});
|
|
|
|
test('非法下载配额被禁止而负数统一为不限', () => {
|
|
assert.strictEqual(normalizeDownloadTrafficQuota('invalid'), 0);
|
|
assert.strictEqual(normalizeDownloadTrafficQuota(-99), -1);
|
|
});
|
|
|
|
test('下载配额被取整并限制在 10TB', () => {
|
|
assert.strictEqual(normalizeDownloadTrafficQuota(10.9), 10);
|
|
assert.strictEqual(normalizeDownloadTrafficQuota(MAX_DOWNLOAD_TRAFFIC_BYTES + 1), MAX_DOWNLOAD_TRAFFIC_BYTES);
|
|
});
|
|
|
|
test('已用流量按有限配额封顶', () => {
|
|
assert.strictEqual(normalizeDownloadTrafficUsed(150, 100), 100);
|
|
assert.strictEqual(normalizeDownloadTrafficUsed(150, -1), 150);
|
|
assert.strictEqual(normalizeDownloadTrafficUsed(-1, 100), 0);
|
|
});
|
|
|
|
test('不限流量状态保留无限剩余额度', () => {
|
|
const state = getDownloadTrafficState({ download_traffic_quota: -1, download_traffic_used: 123 });
|
|
assert.strictEqual(state.isUnlimited, true);
|
|
assert.strictEqual(state.used, 123);
|
|
assert.strictEqual(state.remaining, Number.POSITIVE_INFINITY);
|
|
});
|
|
|
|
test('空用户不会产生配额更新', () => {
|
|
assert.deepStrictEqual(resolveDownloadTrafficPolicyUpdates(null), {
|
|
updates: {},
|
|
hasUpdates: false,
|
|
expired: false,
|
|
resetApplied: false
|
|
});
|
|
});
|
|
|
|
test('非法重置周期会被规范化', () => {
|
|
const result = resolveDownloadTrafficPolicyUpdates({
|
|
download_traffic_quota: 100,
|
|
download_traffic_used: 0,
|
|
download_traffic_reset_cycle: 'yearly'
|
|
});
|
|
assert.strictEqual(result.updates.download_traffic_reset_cycle, 'none');
|
|
});
|
|
|
|
test('周期配额首次运行时记录重置时间', () => {
|
|
const now = new Date(2026, 6, 27, 12, 0, 0);
|
|
const result = resolveDownloadTrafficPolicyUpdates({
|
|
download_traffic_quota: 100,
|
|
download_traffic_used: 10,
|
|
download_traffic_reset_cycle: 'daily',
|
|
download_traffic_last_reset_at: null
|
|
}, now);
|
|
assert.strictEqual(result.updates.download_traffic_last_reset_at, '2026-07-27 12:00:00');
|
|
});
|
|
|
|
test('到达周期时已用流量归零', () => {
|
|
const result = resolveDownloadTrafficPolicyUpdates({
|
|
download_traffic_quota: 100,
|
|
download_traffic_used: 90,
|
|
download_traffic_reset_cycle: 'daily',
|
|
download_traffic_last_reset_at: '2026-07-26 12:00:00'
|
|
}, new Date(2026, 6, 27, 12, 0, 0));
|
|
assert.strictEqual(result.resetApplied, true);
|
|
assert.strictEqual(result.updates.download_traffic_used, 0);
|
|
});
|
|
|
|
test('限时配额到期后恢复为不限流量', () => {
|
|
const result = resolveDownloadTrafficPolicyUpdates({
|
|
download_traffic_quota: 100,
|
|
download_traffic_used: 90,
|
|
download_traffic_quota_expires_at: '2026-07-26 12:00:00',
|
|
download_traffic_reset_cycle: 'daily',
|
|
download_traffic_last_reset_at: '2026-07-26 12:00:00'
|
|
}, new Date(2026, 6, 27, 12, 0, 0));
|
|
assert.strictEqual(result.expired, true);
|
|
assert.strictEqual(result.updates.download_traffic_quota, -1);
|
|
assert.strictEqual(result.updates.download_traffic_used, 0);
|
|
assert.strictEqual(result.updates.download_traffic_quota_expires_at, null);
|
|
});
|
|
|
|
test('Express 错误处理器保留四参数签名', () => {
|
|
assert.strictEqual(expressErrorHandler.length, 4);
|
|
});
|
|
|
|
test('Express 错误处理器返回客户端错误 JSON', () => {
|
|
const response = createResponse();
|
|
const originalConsoleError = console.error;
|
|
console.error = () => {};
|
|
try {
|
|
expressErrorHandler(
|
|
Object.assign(new Error('请求 JSON 无效'), { status: 400 }),
|
|
{ method: 'POST', originalUrl: '/api/login' },
|
|
response,
|
|
() => assert.fail('不应调用 next')
|
|
);
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
assert.strictEqual(response.statusCode, 400);
|
|
assert.deepStrictEqual(response.payload, { success: false, message: '请求 JSON 无效' });
|
|
});
|
|
|
|
test('Express 错误处理器隐藏服务端异常详情', () => {
|
|
const response = createResponse();
|
|
const originalConsoleError = console.error;
|
|
console.error = () => {};
|
|
try {
|
|
expressErrorHandler(
|
|
new Error('sensitive detail'),
|
|
{ method: 'GET', originalUrl: '/api/example' },
|
|
response,
|
|
() => assert.fail('不应调用 next')
|
|
);
|
|
} finally {
|
|
console.error = originalConsoleError;
|
|
}
|
|
assert.strictEqual(response.statusCode, 500);
|
|
assert.deepStrictEqual(response.payload, { success: false, message: '服务器内部错误' });
|
|
});
|
|
|
|
test('响应头已发送时错误处理器继续交给 Express', () => {
|
|
const response = createResponse(true);
|
|
const error = new Error('stream failed');
|
|
let forwarded = null;
|
|
expressErrorHandler(error, {}, response, (received) => {
|
|
forwarded = received;
|
|
});
|
|
assert.strictEqual(forwarded, error);
|
|
});
|
|
|
|
console.log('\n========================================');
|
|
console.log('测试总结');
|
|
console.log('========================================');
|
|
console.log(`通过: ${results.passed}`);
|
|
console.log(`失败: ${results.failed}`);
|
|
|
|
process.exit(results.failed > 0 ? 1 : 0);
|