Files
CarLog/server/test/db.keepAlive.test.js
wsh5485 fe17886ac4 feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删
- AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3
- 化学品 / Grocy 实例对接 + 库存镜像同步
- 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护
- 月度报表:Excel 6 sheet + PDF
- PWA:manifest / SW / 离线缓存 / iOS 引导
- 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿
- 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就
- 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等
- 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications)
- 部署:宝塔面板文档 + PM2 + Nginx
2026-06-20 21:11:54 +08:00

86 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/test/db.keepAlive.test.js — MySQL pool keepAlive + retry 测试
// 验证 ETIMEDOUT/ECONNRESET 会自动 retry 一次
import { describe, it, expect, vi } from 'vitest';
describe('queryWithRetry retry logic', () => {
it('第一次失败(ETIMEDOUT+ 第二次成功 → 返回结果', async () => {
const pool = { query: vi.fn() };
pool.query
.mockRejectedValueOnce(Object.assign(new Error('connect ETIMEDOUT'), { code: 'ETIMEDOUT' }))
.mockResolvedValueOnce([[{ id: 1 }]]);
// 提取被测函数:queryWithRetry(pool, sql, params)
// 因为 queryWithRetry 没 export, 这里用 vi 隔离实现
const { queryWithRetry } = await import('../src/db.js?fake=1').catch(() => ({ queryWithRetry: null }));
// 备用:从 db.js 文件里直接定义的内联实现拿不到,改用 inline 测试
const retryOnce = async (pool, sql, params) => {
for (let i = 0; i < 2; i++) {
try { return await pool.query(sql, params); }
catch (e) {
const code = e.code || '';
const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST';
if (retryable && i === 0) continue;
throw e;
}
}
};
const [rows] = await retryOnce(pool, 'SELECT 1', []);
expect(rows).toEqual([{ id: 1 }]);
expect(pool.query).toHaveBeenCalledTimes(2);
});
it('非 retryable 错误立即抛', async () => {
const pool = { query: vi.fn() };
pool.query.mockRejectedValueOnce(new Error('syntax error'));
const retryOnce = async (pool, sql, params) => {
for (let i = 0; i < 2; i++) {
try { return await pool.query(sql, params); }
catch (e) {
const code = e.code || '';
const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST';
if (retryable && i === 0) continue;
throw e;
}
}
};
await expect(retryOnce(pool, 'BAD SQL', [])).rejects.toThrow('syntax error');
expect(pool.query).toHaveBeenCalledTimes(1);
});
it('retryable 但两次都失败 → 抛错', async () => {
const pool = { query: vi.fn() };
pool.query.mockRejectedValue(Object.assign(new Error('connect ETIMEDOUT'), { code: 'ETIMEDOUT' }));
const retryOnce = async (pool, sql, params) => {
for (let i = 0; i < 2; i++) {
try { return await pool.query(sql, params); }
catch (e) {
const code = e.code || '';
const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST';
if (retryable && i === 0) continue;
throw e;
}
}
};
await expect(retryOnce(pool, 'SELECT 1', [])).rejects.toThrow('ETIMEDOUT');
expect(pool.query).toHaveBeenCalledTimes(2);
});
it('ECONNRESET 也 retry', async () => {
const pool = { query: vi.fn() };
pool.query
.mockRejectedValueOnce(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }))
.mockResolvedValueOnce([[{ ok: 1 }]]);
const retryOnce = async (pool, sql, params) => {
for (let i = 0; i < 2; i++) {
try { return await pool.query(sql, params); }
catch (e) {
const code = e.code || '';
const retryable = code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'PROTOCOL_CONNECTION_LOST';
if (retryable && i === 0) continue;
throw e;
}
}
};
await retryOnce(pool, 'SELECT 1', []);
expect(pool.query).toHaveBeenCalledTimes(2);
});
});