befly 3.8.25 → 3.8.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/config.ts +8 -9
  2. package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
  3. package/hooks/auth.ts +3 -11
  4. package/hooks/cors.ts +1 -4
  5. package/hooks/parser.ts +6 -8
  6. package/hooks/permission.ts +9 -12
  7. package/hooks/validator.ts +6 -9
  8. package/lib/cacheHelper.ts +0 -4
  9. package/lib/{database.ts → connect.ts} +65 -18
  10. package/lib/logger.ts +1 -17
  11. package/lib/redisHelper.ts +6 -5
  12. package/loader/loadApis.ts +3 -3
  13. package/loader/loadHooks.ts +15 -41
  14. package/loader/loadPlugins.ts +10 -16
  15. package/main.ts +25 -28
  16. package/package.json +4 -4
  17. package/plugins/cache.ts +2 -2
  18. package/plugins/cipher.ts +15 -0
  19. package/plugins/config.ts +16 -0
  20. package/plugins/db.ts +7 -17
  21. package/plugins/jwt.ts +15 -0
  22. package/plugins/logger.ts +1 -1
  23. package/plugins/redis.ts +4 -4
  24. package/plugins/tool.ts +50 -0
  25. package/router/api.ts +56 -42
  26. package/router/static.ts +12 -12
  27. package/sync/syncAll.ts +2 -20
  28. package/sync/syncApi.ts +7 -7
  29. package/sync/syncDb/apply.ts +10 -12
  30. package/sync/syncDb/constants.ts +64 -12
  31. package/sync/syncDb/ddl.ts +9 -8
  32. package/sync/syncDb/helpers.ts +7 -119
  33. package/sync/syncDb/schema.ts +16 -19
  34. package/sync/syncDb/sqlite.ts +1 -3
  35. package/sync/syncDb/table.ts +13 -146
  36. package/sync/syncDb/tableCreate.ts +28 -12
  37. package/sync/syncDb/types.ts +126 -0
  38. package/sync/syncDb/version.ts +4 -7
  39. package/sync/syncDb.ts +151 -6
  40. package/sync/syncDev.ts +19 -15
  41. package/sync/syncMenu.ts +87 -75
  42. package/tests/redisHelper.test.ts +15 -16
  43. package/tests/sync-connection.test.ts +189 -0
  44. package/tests/syncDb-apply.test.ts +288 -0
  45. package/tests/syncDb-constants.test.ts +151 -0
  46. package/tests/syncDb-ddl.test.ts +206 -0
  47. package/tests/syncDb-helpers.test.ts +113 -0
  48. package/tests/syncDb-schema.test.ts +178 -0
  49. package/tests/syncDb-types.test.ts +130 -0
  50. package/tsconfig.json +2 -2
  51. package/types/api.d.ts +1 -1
  52. package/types/befly.d.ts +23 -21
  53. package/types/common.d.ts +0 -29
  54. package/types/context.d.ts +8 -6
  55. package/types/hook.d.ts +3 -4
  56. package/types/plugin.d.ts +3 -0
  57. package/hooks/errorHandler.ts +0 -23
  58. package/hooks/requestId.ts +0 -24
  59. package/hooks/requestLogger.ts +0 -25
  60. package/hooks/responseFormatter.ts +0 -64
  61. package/router/root.ts +0 -56
  62. package/sync/syncDb/index.ts +0 -164
package/sync/syncDev.ts CHANGED
@@ -7,9 +7,12 @@
7
7
  * - 表名: addon_admin_admin
8
8
  */
9
9
 
10
- import { Database } from '../lib/database.js';
11
- import { Cipher } from '../lib/cipher.js';
10
+ import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-util';
11
+
12
12
  import { Logger } from '../lib/logger.js';
13
+ import { Cipher } from '../lib/cipher.js';
14
+ import { Connect } from '../lib/connect.js';
15
+ import { DbHelper } from '../lib/dbHelper.js';
13
16
  import type { SyncDevOptions, SyncDevStats, BeflyOptions } from '../types/index.js';
14
17
 
15
18
  /**
@@ -22,33 +25,34 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
22
25
  return;
23
26
  }
24
27
 
25
- const devPassword = config.devPassword;
26
- const devEmail = config.devEmail || 'dev@qq.com';
27
-
28
- if (!devPassword) {
28
+ if (!config.devPassword) {
29
+ // 未配置开发者密码,跳过同步
29
30
  return;
30
31
  }
31
32
 
32
33
  // 连接数据库(SQL + Redis)
33
- await Database.connect();
34
+ await Connect.connect(config);
34
35
 
35
- const helper = Database.getDbHelper();
36
+ const helper = Connect.getDbHelper();
36
37
 
37
38
  // 检查 addon_admin_admin 表是否存在
38
39
  const existAdmin = await helper.tableExists('addon_admin_admin');
39
40
  if (!existAdmin) {
41
+ Logger.debug('[SyncDev] 表 addon_admin_admin 不存在,跳过开发者账号同步');
40
42
  return;
41
43
  }
42
44
 
43
45
  // 检查 addon_admin_role 表是否存在
44
46
  const existRole = await helper.tableExists('addon_admin_role');
45
47
  if (!existRole) {
48
+ Logger.debug('[SyncDev] 表 addon_admin_role 不存在,跳过开发者账号同步');
46
49
  return;
47
50
  }
48
51
 
49
52
  // 检查 addon_admin_menu 表是否存在
50
53
  const existMenu = await helper.tableExists('addon_admin_menu');
51
54
  if (!existMenu) {
55
+ Logger.debug('[SyncDev] 表 addon_admin_menu 不存在,跳过开发者账号同步');
52
56
  return;
53
57
  }
54
58
 
@@ -59,6 +63,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
59
63
  });
60
64
 
61
65
  if (!allMenus || !Array.isArray(allMenus)) {
66
+ Logger.debug('[SyncDev] 菜单数据为空,跳过开发者账号同步');
62
67
  return;
63
68
  }
64
69
 
@@ -113,13 +118,12 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
113
118
  }
114
119
 
115
120
  // 使用 bcrypt 加密密码
116
- const hashed = await Cipher.hashPassword(devPassword);
121
+ const hashed = await Cipher.hashPassword(config.devPassword);
117
122
 
118
123
  // 准备开发管理员数据
119
124
  const devData = {
120
- name: '开发者',
121
125
  nickname: '开发者',
122
- email: devEmail,
126
+ email: config.devEmail,
123
127
  username: 'dev',
124
128
  password: hashed,
125
129
  roleId: devRole.id,
@@ -130,14 +134,14 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
130
134
  // 查询现有账号
131
135
  const existing = await helper.getOne({
132
136
  table: 'addon_admin_admin',
133
- where: { email: devEmail }
137
+ where: { email: config.devEmail }
134
138
  });
135
139
 
136
140
  if (existing) {
137
141
  // 更新现有账号
138
142
  await helper.updData({
139
143
  table: 'addon_admin_admin',
140
- where: { email: devEmail },
144
+ where: { email: config.devEmail },
141
145
  data: devData
142
146
  });
143
147
  } else {
@@ -167,7 +171,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
167
171
  fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
168
172
  });
169
173
 
170
- const redis = Database.getRedis();
174
+ const redis = Connect.getRedis();
171
175
 
172
176
  // 为每个角色缓存接口权限
173
177
  for (const role of roles) {
@@ -201,6 +205,6 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
201
205
  Logger.error('同步开发者管理员失败', error);
202
206
  throw error;
203
207
  } finally {
204
- await Database?.disconnect();
208
+ await Connect.disconnect();
205
209
  }
206
210
  }
package/sync/syncMenu.ts CHANGED
@@ -1,50 +1,39 @@
1
1
  /**
2
2
  * SyncMenu 命令 - 同步菜单数据到数据库
3
- * 说明:根据 menu.json 配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
3
+ * 说明:根据配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
4
4
  *
5
5
  * 流程:
6
- * 1. 扫描项目根目录和所有 addon 的 menu.json 配置文件
7
- * 2. 项目的 menu.json 优先级最高,可以覆盖 addon 的菜单配置
8
- * 3. 文件不存在或格式错误时默认为空数组
9
- * 4. 根据菜单的 path 字段检查是否存在
10
- * 5. 存在则更新其他字段(name、sort、type、pid)
11
- * 6. 不存在则新增菜单记录
12
- * 7. 强制删除配置中不存在的菜单记录
6
+ * 1. 扫描所有 addon 的 addon.config.js/ts 配置文件
7
+ * 2. 扫描项目根目录的 app.config.js/ts 配置文件
8
+ * 3. 项目的 app.config 优先级最高,可以覆盖 addon 的菜单配置
9
+ * 4. 文件不存在或格式错误时默认为空数组
10
+ * 5. 根据菜单的 path 字段检查是否存在
11
+ * 6. 存在则更新其他字段(name、sort、type、pid)
12
+ * 7. 不存在则新增菜单记录
13
+ * 8. 强制删除配置中不存在的菜单记录
13
14
  * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
14
15
  */
15
16
 
16
17
  import { join } from 'pathe';
17
- import { existsSync } from 'node:fs';
18
- import { Database } from '../lib/database.js';
18
+ import { cloneDeep } from 'es-toolkit';
19
+ import { Connect } from '../lib/connect.js';
19
20
  import { RedisHelper } from '../lib/redisHelper.js';
20
- import { scanAddons, getAddonDir } from 'befly-util';
21
+ import { scanAddons, getAddonDir, scanConfig } from 'befly-util';
21
22
  import { Logger } from '../lib/logger.js';
22
23
  import { projectDir } from '../paths.js';
23
24
 
24
25
  import type { SyncMenuOptions, MenuConfig, BeflyOptions } from '../types/index.js';
25
26
 
26
27
  /**
27
- * 读取菜单配置文件
28
- * 如果文件不存在或不是数组格式,返回空数组
28
+ * 递归转换菜单路径
29
+ * @param menu 菜单对象(会被修改)
30
+ * @param transform 路径转换函数
29
31
  */
30
- async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
31
- try {
32
- if (!existsSync(filePath)) {
33
- return [];
34
- }
35
-
36
- const content = await import(filePath, { with: { type: 'json' } });
37
-
38
- // 验证是否为数组
39
- if (!Array.isArray(content.default)) {
40
- return [];
41
- }
42
-
43
- return content.default;
44
- } catch (error: any) {
45
- Logger.warn(`读取菜单配置失败 ${filePath}: ${error.message}`);
46
- return [];
32
+ function transformMenuPaths(menu: MenuConfig, transform: (path: string) => string): void {
33
+ if (menu.path && menu.path.startsWith('/')) {
34
+ menu.path = transform(menu.path);
47
35
  }
36
+ menu.children?.forEach((child) => transformMenuPaths(child, transform));
48
37
  }
49
38
 
50
39
  /**
@@ -57,25 +46,15 @@ async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
57
46
  */
58
47
  function addAddonPrefix(menus: MenuConfig[], addonName: string): MenuConfig[] {
59
48
  return menus.map((menu) => {
60
- const newMenu = { ...menu };
61
-
62
- // 处理当前菜单的 path(包括根路径 /)
63
- if (newMenu.path && newMenu.path.startsWith('/')) {
64
- newMenu.path = `/addon/${addonName}${newMenu.path}`;
65
- }
66
-
67
- // 递归处理子菜单
68
- if (newMenu.children && newMenu.children.length > 0) {
69
- newMenu.children = addAddonPrefix(newMenu.children, addonName);
70
- }
71
-
72
- return newMenu;
49
+ const cloned = cloneDeep(menu);
50
+ transformMenuPaths(cloned, (path) => `/addon/${addonName}${path}`);
51
+ return cloned;
73
52
  });
74
53
  }
75
54
 
76
55
  /**
77
56
  * 合并菜单配置
78
- * 优先级:项目 menu.json > addon menu.json
57
+ * 优先级:项目 package.json > addon package.json
79
58
  * 支持三级菜单结构:父级、子级、孙级
80
59
  */
81
60
  function mergeMenuConfigs(allMenus: Array<{ menus: MenuConfig[]; addonName: string }>): MenuConfig[] {
@@ -251,6 +230,58 @@ async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Pro
251
230
  }
252
231
  }
253
232
 
233
+ /**
234
+ * 加载所有菜单配置(addon + 项目)
235
+ * @returns 菜单配置数组
236
+ */
237
+ async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; addonName: string }>> {
238
+ const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
239
+
240
+ // 1. 加载所有 addon 配置
241
+ const addonNames = scanAddons();
242
+
243
+ for (const addonName of addonNames) {
244
+ try {
245
+ const addonDir = getAddonDir(addonName, '');
246
+ const addonConfigData = await scanConfig({
247
+ dirs: [addonDir],
248
+ files: ['addon.config'],
249
+ mode: 'first',
250
+ paths: ['menus']
251
+ });
252
+
253
+ const addonMenus = addonConfigData?.menus || [];
254
+ if (Array.isArray(addonMenus) && addonMenus.length > 0) {
255
+ // 为 addon 菜单添加路径前缀
256
+ const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
257
+ allMenus.push({ menus: menusWithPrefix, addonName: addonName });
258
+ }
259
+ } catch (error: any) {
260
+ Logger.warn(`读取 addon 配置失败 ${addonName}: ${error.message}`);
261
+ }
262
+ }
263
+
264
+ // 2. 加载项目配置
265
+ try {
266
+ const appConfigData = await scanConfig({
267
+ dirs: [projectDir],
268
+ files: ['app.config'],
269
+ mode: 'first',
270
+ paths: ['menus']
271
+ });
272
+
273
+ const appMenus = appConfigData?.menus || [];
274
+ if (Array.isArray(appMenus) && appMenus.length > 0) {
275
+ // 项目菜单不添加前缀
276
+ allMenus.push({ menus: appMenus, addonName: 'app' });
277
+ }
278
+ } catch (error: any) {
279
+ Logger.warn(`读取项目配置失败: ${error.message}`);
280
+ }
281
+
282
+ return allMenus;
283
+ }
284
+
254
285
  /**
255
286
  * SyncMenu 命令主函数
256
287
  */
@@ -261,37 +292,18 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
261
292
  return;
262
293
  }
263
294
 
264
- // 1. 扫描所有 addon menu.json 配置文件
265
- const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
266
-
267
- const addonNames = scanAddons();
268
-
269
- for (const addonName of addonNames) {
270
- const addonMenuPath = getAddonDir(addonName, 'menu.json');
271
- if (existsSync(addonMenuPath)) {
272
- const addonMenus = await readMenuConfig(addonMenuPath);
273
- if (addonMenus.length > 0) {
274
- // 为 addon 菜单添加路径前缀
275
- const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
276
- allMenus.push({ menus: menusWithPrefix, addonName: addonName });
277
- }
278
- }
279
- }
295
+ // 1. 加载所有菜单配置(addon + 项目)
296
+ const allMenus = await loadMenuConfigs();
280
297
 
281
- // 2. 读取项目根目录的 menu.json(优先级最高,不添加前缀)
282
- const projectMenuPath = join(projectDir, 'menu.json');
283
- const projectMenus = await readMenuConfig(projectMenuPath);
284
- if (projectMenus.length > 0) {
285
- allMenus.push({ menus: projectMenus, addonName: 'project' });
286
- } // 3. 合并菜单配置(项目配置优先)
298
+ // 2. 合并菜单配置
287
299
  const mergedMenus = mergeMenuConfigs(allMenus);
288
300
 
289
301
  // 连接数据库(SQL + Redis)
290
- await Database.connect();
302
+ await Connect.connect(config);
291
303
 
292
- const helper = Database.getDbHelper();
304
+ const helper = Connect.getDbHelper();
293
305
 
294
- // 4. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
306
+ // 3. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
295
307
  const exists = await helper.tableExists('addon_admin_menu');
296
308
 
297
309
  if (!exists) {
@@ -299,23 +311,23 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
299
311
  return;
300
312
  }
301
313
 
302
- // 5. 收集配置文件中所有菜单的 path
314
+ // 4. 收集配置文件中所有菜单的 path
303
315
  const configPaths = collectPaths(mergedMenus);
304
316
 
305
- // 6. 同步菜单
317
+ // 5. 同步菜单
306
318
  await syncMenus(helper, mergedMenus);
307
319
 
308
- // 7. 删除文件中不存在的菜单(强制删除)
320
+ // 6. 删除文件中不存在的菜单(强制删除)
309
321
  await deleteObsoleteRecords(helper, configPaths);
310
322
 
311
- // 8. 获取最终菜单数据(用于缓存)
323
+ // 7. 获取最终菜单数据(用于缓存)
312
324
  const allMenusData = await helper.getAll({
313
325
  table: 'addon_admin_menu',
314
326
  fields: ['id', 'pid', 'name', 'path', 'sort'],
315
327
  orderBy: ['sort#ASC', 'id#ASC']
316
328
  });
317
329
 
318
- // 9. 缓存菜单数据到 Redis
330
+ // 8. 缓存菜单数据到 Redis
319
331
  try {
320
332
  const redisHelper = new RedisHelper();
321
333
  await redisHelper.setObject('menus:all', allMenusData);
@@ -326,6 +338,6 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
326
338
  Logger.error('菜单同步失败', error);
327
339
  throw error;
328
340
  } finally {
329
- await Database?.disconnect();
341
+ await Connect.disconnect();
330
342
  }
331
343
  }
@@ -3,29 +3,24 @@
3
3
  * 测试 Redis 操作功能
4
4
  */
5
5
 
6
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
7
- import { Database } from '../lib/database.js';
8
- import { RedisHelper } from '../lib/redisHelper.js';
6
+ import { describe, expect, it, test, beforeAll, afterAll } from 'bun:test';
7
+ import { RedisClient } from 'bun';
8
+
9
9
  import { defaultOptions } from '../config.js';
10
+ import { Connect } from '../lib/connect.js';
11
+ import { RedisHelper } from '../lib/redisHelper.js';
10
12
 
11
13
  let redis: RedisHelper;
12
14
 
13
15
  beforeAll(async () => {
14
- // 使用项目默认配置连接 Redis
15
- await Database.connectRedis(defaultOptions.redis);
16
- // 使用项目配置的 prefix
17
- redis = new RedisHelper(defaultOptions.redis.prefix);
16
+ // 连接 Redis
17
+ await Connect.connectRedis(defaultOptions.redis);
18
+ redis = new RedisHelper();
18
19
  });
19
20
 
20
21
  afterAll(async () => {
21
- // 清理测试数据
22
- await redis.del('test:string');
23
- await redis.del('test:object');
24
- await redis.del('test:set');
25
- await redis.del('test:ttl');
26
- await redis.del('test:expire');
27
-
28
- await Database.disconnectRedis();
22
+ // 断开 Redis 连接
23
+ await Connect.disconnectRedis();
29
24
  });
30
25
 
31
26
  describe('RedisHelper - 字符串操作', () => {
@@ -102,11 +97,15 @@ describe('RedisHelper - 对象操作', () => {
102
97
 
103
98
  describe('RedisHelper - Set 操作', () => {
104
99
  test('sadd - 添加成员到 Set', async () => {
105
- const count = await redis.sadd('test:set', ['member1', 'member2', 'member3']);
100
+ // 先清除,确保测试隔离
101
+ await redis.del('test:set:sadd');
102
+ const count = await redis.sadd('test:set:sadd', ['member1', 'member2', 'member3']);
106
103
  expect(count).toBeGreaterThan(0);
104
+ await redis.del('test:set:sadd');
107
105
  });
108
106
 
109
107
  test('sismember - 检查成员是否存在', async () => {
108
+ await redis.del('test:set');
110
109
  await redis.sadd('test:set', ['member1']);
111
110
 
112
111
  const exists = await redis.sismember('test:set', 'member1');
@@ -0,0 +1,189 @@
1
+ /**
2
+ * sync 模块连接管理集成测试
3
+ * 验证数据库连接的正确关闭
4
+ */
5
+ import { describe, test, expect, afterEach } from 'bun:test';
6
+
7
+ import { Connect } from '../lib/connect.js';
8
+
9
+ describe('sync 模块连接管理', () => {
10
+ afterEach(() => {
11
+ // 每个测试后重置连接状态
12
+ Connect.__reset();
13
+ });
14
+
15
+ describe('Connect.isConnected', () => {
16
+ test('初始状态应该都是未连接', () => {
17
+ const status = Connect.isConnected();
18
+ expect(status.sql).toBe(false);
19
+ expect(status.redis).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe('Connect.disconnect', () => {
24
+ test('disconnect 应该能安全地关闭未连接的状态', async () => {
25
+ // 即使没有连接,disconnect 也不应该抛出错误
26
+ await expect(Connect.disconnect()).resolves.toBeUndefined();
27
+ });
28
+
29
+ test('disconnectSql 应该能安全地关闭未连接的状态', async () => {
30
+ await expect(Connect.disconnectSql()).resolves.toBeUndefined();
31
+ });
32
+
33
+ test('disconnectRedis 应该能安全地关闭未连接的状态', async () => {
34
+ await expect(Connect.disconnectRedis()).resolves.toBeUndefined();
35
+ });
36
+ });
37
+
38
+ describe('Connect.getSql', () => {
39
+ test('未连接时应该抛出错误', () => {
40
+ expect(() => Connect.getSql()).toThrow('SQL 客户端未连接');
41
+ });
42
+ });
43
+
44
+ describe('Connect.getRedis', () => {
45
+ test('未连接时应该抛出错误', () => {
46
+ expect(() => Connect.getRedis()).toThrow('Redis 客户端未连接');
47
+ });
48
+ });
49
+
50
+ describe('Connect.getDbHelper', () => {
51
+ test('未连接时应该抛出错误', () => {
52
+ expect(() => Connect.getDbHelper()).toThrow('SQL 客户端未连接');
53
+ });
54
+ });
55
+
56
+ describe('Mock 连接测试', () => {
57
+ test('__setMockSql 应该设置 mock SQL 客户端', () => {
58
+ const mockSql = { close: async () => {} } as any;
59
+ Connect.__setMockSql(mockSql);
60
+
61
+ const status = Connect.isConnected();
62
+ expect(status.sql).toBe(true);
63
+ expect(status.redis).toBe(false);
64
+ });
65
+
66
+ test('__setMockRedis 应该设置 mock Redis 客户端', () => {
67
+ const mockRedis = { close: () => {} } as any;
68
+ Connect.__setMockRedis(mockRedis);
69
+
70
+ const status = Connect.isConnected();
71
+ expect(status.sql).toBe(false);
72
+ expect(status.redis).toBe(true);
73
+ });
74
+
75
+ test('__reset 应该重置所有连接', () => {
76
+ const mockSql = { close: async () => {} } as any;
77
+ const mockRedis = { close: () => {} } as any;
78
+ Connect.__setMockSql(mockSql);
79
+ Connect.__setMockRedis(mockRedis);
80
+
81
+ expect(Connect.isConnected().sql).toBe(true);
82
+ expect(Connect.isConnected().redis).toBe(true);
83
+
84
+ Connect.__reset();
85
+
86
+ expect(Connect.isConnected().sql).toBe(false);
87
+ expect(Connect.isConnected().redis).toBe(false);
88
+ });
89
+
90
+ test('disconnect 应该正确关闭 mock 连接', async () => {
91
+ let sqlClosed = false;
92
+ let redisClosed = false;
93
+
94
+ const mockSql = {
95
+ close: async () => {
96
+ sqlClosed = true;
97
+ }
98
+ } as any;
99
+ const mockRedis = {
100
+ close: () => {
101
+ redisClosed = true;
102
+ }
103
+ } as any;
104
+
105
+ Connect.__setMockSql(mockSql);
106
+ Connect.__setMockRedis(mockRedis);
107
+
108
+ await Connect.disconnect();
109
+
110
+ expect(sqlClosed).toBe(true);
111
+ expect(redisClosed).toBe(true);
112
+ expect(Connect.isConnected().sql).toBe(false);
113
+ expect(Connect.isConnected().redis).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('连接异常处理', () => {
118
+ test('disconnectSql 应该处理关闭时的错误', async () => {
119
+ const mockSql = {
120
+ close: async () => {
121
+ throw new Error('Close error');
122
+ }
123
+ } as any;
124
+
125
+ Connect.__setMockSql(mockSql);
126
+
127
+ // 不应该抛出错误,只是记录日志
128
+ await expect(Connect.disconnectSql()).resolves.toBeUndefined();
129
+ expect(Connect.isConnected().sql).toBe(false);
130
+ });
131
+
132
+ test('disconnectRedis 应该处理关闭时的错误', async () => {
133
+ const mockRedis = {
134
+ close: () => {
135
+ throw new Error('Close error');
136
+ }
137
+ } as any;
138
+
139
+ Connect.__setMockRedis(mockRedis);
140
+
141
+ // 不应该抛出错误,只是记录日志
142
+ await expect(Connect.disconnectRedis()).resolves.toBeUndefined();
143
+ expect(Connect.isConnected().redis).toBe(false);
144
+ });
145
+ });
146
+
147
+ describe('Connect.getStatus', () => {
148
+ test('未连接时返回正确的状态', () => {
149
+ const status = Connect.getStatus();
150
+
151
+ expect(status.sql.connected).toBe(false);
152
+ expect(status.sql.connectedAt).toBeNull();
153
+ expect(status.sql.uptime).toBeNull();
154
+ expect(status.sql.poolMax).toBe(1);
155
+
156
+ expect(status.redis.connected).toBe(false);
157
+ expect(status.redis.connectedAt).toBeNull();
158
+ expect(status.redis.uptime).toBeNull();
159
+ });
160
+
161
+ test('Mock 连接后返回正确的状态', () => {
162
+ const mockSql = { close: async () => {} } as any;
163
+ const mockRedis = { close: () => {} } as any;
164
+
165
+ Connect.__setMockSql(mockSql);
166
+ Connect.__setMockRedis(mockRedis);
167
+
168
+ const status = Connect.getStatus();
169
+
170
+ // Mock 不会设置连接时间,但连接状态应该为 true
171
+ expect(status.sql.connected).toBe(true);
172
+ expect(status.redis.connected).toBe(true);
173
+ });
174
+
175
+ test('__reset 应该重置所有状态包括连接时间', () => {
176
+ const mockSql = { close: async () => {} } as any;
177
+ Connect.__setMockSql(mockSql);
178
+
179
+ expect(Connect.getStatus().sql.connected).toBe(true);
180
+
181
+ Connect.__reset();
182
+
183
+ const status = Connect.getStatus();
184
+ expect(status.sql.connected).toBe(false);
185
+ expect(status.sql.connectedAt).toBeNull();
186
+ expect(status.sql.poolMax).toBe(1);
187
+ });
188
+ });
189
+ });