befly 3.8.27 → 3.8.30

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 (65) hide show
  1. package/README.md +8 -6
  2. package/checks/checkApi.ts +2 -1
  3. package/checks/checkTable.ts +3 -2
  4. package/hooks/parser.ts +5 -3
  5. package/hooks/permission.ts +12 -5
  6. package/lib/cacheHelper.ts +76 -62
  7. package/lib/connect.ts +8 -35
  8. package/lib/dbHelper.ts +14 -11
  9. package/lib/jwt.ts +58 -437
  10. package/lib/logger.ts +76 -197
  11. package/lib/redisHelper.ts +163 -1
  12. package/lib/sqlBuilder.ts +2 -1
  13. package/lib/validator.ts +9 -8
  14. package/loader/loadApis.ts +4 -7
  15. package/loader/loadHooks.ts +2 -2
  16. package/loader/loadPlugins.ts +4 -4
  17. package/main.ts +4 -17
  18. package/package.json +10 -9
  19. package/paths.ts +0 -6
  20. package/plugins/db.ts +2 -2
  21. package/plugins/jwt.ts +5 -5
  22. package/plugins/redis.ts +1 -1
  23. package/router/api.ts +2 -2
  24. package/router/static.ts +1 -2
  25. package/sync/syncAll.ts +2 -2
  26. package/sync/syncApi.ts +10 -7
  27. package/sync/syncDb/apply.ts +11 -11
  28. package/sync/syncDb/constants.ts +61 -12
  29. package/sync/syncDb/ddl.ts +7 -7
  30. package/sync/syncDb/helpers.ts +3 -3
  31. package/sync/syncDb/schema.ts +16 -19
  32. package/sync/syncDb/table.ts +6 -5
  33. package/sync/syncDb/tableCreate.ts +7 -7
  34. package/sync/syncDb/types.ts +3 -2
  35. package/sync/syncDb/version.ts +4 -4
  36. package/sync/syncDb.ts +11 -10
  37. package/sync/syncDev.ts +10 -48
  38. package/sync/syncMenu.ts +11 -8
  39. package/tests/cacheHelper.test.ts +327 -0
  40. package/tests/dbHelper-columns.test.ts +5 -20
  41. package/tests/dbHelper-execute.test.ts +14 -68
  42. package/tests/fields-redis-cache.test.ts +5 -3
  43. package/tests/integration.test.ts +15 -26
  44. package/tests/jwt.test.ts +36 -94
  45. package/tests/logger.test.ts +32 -34
  46. package/tests/redisHelper.test.ts +270 -0
  47. package/tests/redisKeys.test.ts +76 -0
  48. package/tests/sync-connection.test.ts +0 -6
  49. package/tests/syncDb-apply.test.ts +3 -2
  50. package/tests/syncDb-constants.test.ts +15 -14
  51. package/tests/syncDb-ddl.test.ts +3 -2
  52. package/tests/syncDb-helpers.test.ts +3 -2
  53. package/tests/syncDb-schema.test.ts +3 -3
  54. package/tests/syncDb-types.test.ts +3 -2
  55. package/tests/util.test.ts +5 -1
  56. package/types/befly.d.ts +2 -15
  57. package/types/common.d.ts +11 -93
  58. package/types/database.d.ts +216 -5
  59. package/types/index.ts +1 -0
  60. package/types/logger.d.ts +11 -41
  61. package/types/table.d.ts +213 -0
  62. package/hooks/_rateLimit.ts +0 -64
  63. package/lib/regexAliases.ts +0 -59
  64. package/lib/xml.ts +0 -383
  65. package/tests/xml.test.ts +0 -101
package/tests/jwt.test.ts CHANGED
@@ -1,122 +1,64 @@
1
- import { describe, test, expect, beforeAll } from 'bun:test';
1
+ import { describe, test, expect } from 'bun:test';
2
2
  import { Jwt } from '../lib/jwt';
3
3
 
4
- beforeAll(() => {
5
- Jwt.configure({
6
- secret: 'test-secret',
7
- algorithm: 'HS256',
8
- expiresIn: '7d'
9
- });
4
+ const jwt = new Jwt({
5
+ secret: 'test-secret',
6
+ algorithm: 'HS256',
7
+ expiresIn: '7d'
10
8
  });
11
9
 
12
- describe('JWT - 签名和验证', () => {
13
- test('签名创建 token', async () => {
14
- const token = await Jwt.sign({ userId: 1, name: 'test' });
10
+ describe('JWT - sign', () => {
11
+ test('签名创建 token', () => {
12
+ const token = jwt.sign({ userId: 1, name: 'test' });
15
13
  expect(typeof token).toBe('string');
16
14
  expect(token.split('.').length).toBe(3);
17
15
  });
18
16
 
19
- test('验证有效 token', async () => {
20
- const token = await Jwt.sign({ userId: 123, email: 'test@test.com' });
21
- const decoded = await Jwt.verify(token);
22
- expect(decoded.userId).toBe(123);
23
- expect(decoded.email).toBe('test@test.com');
17
+ test('自定义密钥', () => {
18
+ const secret = 'custom-secret';
19
+ const token = jwt.sign({ userId: 1 }, { secret: secret });
20
+ const decoded = jwt.verify(token, { secret: secret });
21
+ expect(decoded.userId).toBe(1);
24
22
  });
23
+ });
25
24
 
26
- test('验证过期 token 抛出错误', async () => {
27
- const token = await Jwt.sign({ userId: 1 }, { expiresIn: '-1s' });
28
- expect(Jwt.verify(token)).rejects.toThrow('Token 已过期');
25
+ describe('JWT - verify', () => {
26
+ test('验证有效 token', () => {
27
+ const token = jwt.sign({ userId: 123, email: 'test@test.com' });
28
+ const decoded = jwt.verify(token);
29
+ expect(decoded.userId).toBe(123);
30
+ expect(decoded.email).toBe('test@test.com');
29
31
  });
30
32
 
31
- test('忽略过期验证', async () => {
32
- const token = await Jwt.sign({ userId: 1 }, { expiresIn: '-1s' });
33
- const decoded = await Jwt.verify(token, { ignoreExpiration: true });
34
- expect(decoded.userId).toBe(1);
33
+ test('验证过期 token 抛出错误', () => {
34
+ const token = jwt.sign({ userId: 1 }, { expiresIn: '-1s' });
35
+ expect(() => jwt.verify(token)).toThrow();
35
36
  });
36
37
 
37
- test('自定义密钥', async () => {
38
- const secret = 'custom-secret';
39
- const token = await Jwt.sign({ userId: 1 }, { secret });
40
- const decoded = await Jwt.verify(token, { secret });
38
+ test('忽略过期验证', () => {
39
+ const token = jwt.sign({ userId: 1 }, { expiresIn: '-1s' });
40
+ const decoded = jwt.verify(token, { ignoreExpiration: true });
41
41
  expect(decoded.userId).toBe(1);
42
42
  });
43
43
 
44
- test('密钥不匹配验证失败', async () => {
45
- const token = await Jwt.sign({ userId: 1 }, { secret: 'secret1' });
46
- expect(Jwt.verify(token, { secret: 'secret2' })).rejects.toThrow('Token 签名无效');
44
+ test('密钥不匹配验证失败', () => {
45
+ const token = jwt.sign({ userId: 1 }, { secret: 'secret1' });
46
+ expect(() => jwt.verify(token, { secret: 'secret2' })).toThrow();
47
47
  });
48
48
  });
49
49
 
50
- describe('JWT - 解码', () => {
51
- test('解码不验证签名', async () => {
52
- const token = await Jwt.sign({ userId: 456 });
53
- const decoded = Jwt.decode(token);
50
+ describe('JWT - decode', () => {
51
+ test('解码不验证签名', () => {
52
+ const token = jwt.sign({ userId: 456 });
53
+ const decoded = jwt.decode(token);
54
54
  expect(decoded.userId).toBe(456);
55
55
  });
56
56
 
57
- test('解码完整信息', async () => {
58
- const token = await Jwt.sign({ userId: 1 });
59
- const decoded = Jwt.decode(token, true);
57
+ test('解码完整信息', () => {
58
+ const token = jwt.sign({ userId: 1 });
59
+ const decoded = jwt.decode(token, true);
60
60
  expect(decoded.header).toBeDefined();
61
61
  expect(decoded.payload).toBeDefined();
62
62
  expect(decoded.signature).toBeDefined();
63
63
  });
64
64
  });
65
-
66
- describe('JWT - 时间相关', () => {
67
- test('获取剩余时间', async () => {
68
- const token = await Jwt.sign({ userId: 1 }, { expiresIn: '1h' });
69
- const remaining = Jwt.getTimeToExpiry(token);
70
- expect(remaining).toBeGreaterThan(3500);
71
- });
72
-
73
- test('检查是否过期', async () => {
74
- const valid = await Jwt.sign({ userId: 1 }, { expiresIn: '1h' });
75
- expect(Jwt.isExpired(valid)).toBe(false);
76
-
77
- const expired = await Jwt.sign({ userId: 1 }, { expiresIn: '-1s' });
78
- expect(Jwt.isExpired(expired)).toBe(true);
79
- });
80
-
81
- test('检查即将过期', async () => {
82
- const longToken = await Jwt.sign({ userId: 1 }, { expiresIn: '1h' });
83
- expect(Jwt.isNearExpiry(longToken, 300)).toBe(false);
84
-
85
- const shortToken = await Jwt.sign({ userId: 1 }, { expiresIn: '2m' });
86
- expect(Jwt.isNearExpiry(shortToken, 300)).toBe(true);
87
- });
88
- });
89
-
90
- describe('JWT - 权限检查', () => {
91
- test('检查角色', () => {
92
- const payload = { userId: 1, role: 'admin' };
93
- expect(Jwt.hasRole(payload, 'admin')).toBe(true);
94
- expect(Jwt.hasRole(payload, 'user')).toBe(false);
95
- });
96
-
97
- test('检查权限', () => {
98
- const payload = { userId: 1, permissions: ['read', 'write'] };
99
- expect(Jwt.hasPermission(payload, 'read')).toBe(true);
100
- expect(Jwt.hasPermission(payload, 'delete')).toBe(false);
101
- });
102
-
103
- test('检查所有权限', () => {
104
- const payload = { userId: 1, permissions: ['read', 'write', 'delete'] };
105
- expect(Jwt.hasAllPermissions(payload, ['read', 'write'])).toBe(true);
106
- expect(Jwt.hasAllPermissions(payload, ['read', 'admin'])).toBe(false);
107
- });
108
- });
109
-
110
- describe('JWT - 快捷方法', () => {
111
- test('create 和 check', async () => {
112
- const token = await Jwt.create({ userId: 789 });
113
- const decoded = await Jwt.check(token);
114
- expect(decoded.userId).toBe(789);
115
- });
116
-
117
- test('parse 方法', async () => {
118
- const token = await Jwt.create({ userId: 1 });
119
- const decoded = Jwt.parse(token);
120
- expect(decoded.userId).toBe(1);
121
- });
122
- });
@@ -1,4 +1,4 @@
1
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
1
+ import { describe, test, expect, beforeAll, afterAll, afterEach } from 'bun:test';
2
2
  import { Logger } from '../lib/logger';
3
3
  import { existsSync, mkdirSync, rmSync } from 'node:fs';
4
4
  import { join } from 'node:path';
@@ -16,79 +16,77 @@ beforeAll(() => {
16
16
  });
17
17
  });
18
18
 
19
- afterAll(() => {
19
+ afterAll(async () => {
20
+ // 延迟清理,等待 pino-roll 完成写入
21
+ await new Promise((resolve) => setTimeout(resolve, 500));
20
22
  if (existsSync(testLogDir)) {
21
23
  rmSync(testLogDir, { recursive: true, force: true });
22
24
  }
23
25
  });
24
26
 
25
- describe('Logger - 基本功能', () => {
26
- test('记录 info 日志', async () => {
27
- await Logger.log('info', 'Test info message');
27
+ describe('Logger - 纯字符串消息', () => {
28
+ test('info(msg)', () => {
29
+ Logger.info('Test info message');
28
30
  expect(true).toBe(true);
29
31
  });
30
32
 
31
- test('记录 warn 日志', async () => {
32
- await Logger.log('warn', 'Test warning');
33
+ test('warn(msg)', () => {
34
+ Logger.warn('Test warning');
33
35
  expect(true).toBe(true);
34
36
  });
35
37
 
36
- test('记录 error 日志', async () => {
37
- await Logger.log('error', 'Test error');
38
+ test('error(msg)', () => {
39
+ Logger.error('Test error');
38
40
  expect(true).toBe(true);
39
41
  });
40
42
 
41
- test('记录 debug 日志', async () => {
42
- await Logger.log('debug', 'Test debug');
43
- expect(true).toBe(true);
44
- });
45
-
46
- test('记录 success 日志', async () => {
47
- await Logger.success('Test success');
43
+ test('debug(msg)', () => {
44
+ Logger.debug('Test debug');
48
45
  expect(true).toBe(true);
49
46
  });
50
47
  });
51
48
 
52
- describe('Logger - 不同类型消息', () => {
53
- test('记录字符串', async () => {
54
- await Logger.log('info', 'String message');
49
+ describe('Logger - 对象 + 消息 (pino 原生格式)', () => {
50
+ test('info(obj, msg)', () => {
51
+ Logger.info({ userId: 1, action: 'login' }, 'User action');
55
52
  expect(true).toBe(true);
56
53
  });
57
54
 
58
- test('记录数字', async () => {
59
- await Logger.log('info', 12345);
55
+ test('warn(obj, msg)', () => {
56
+ Logger.warn({ ip: '127.0.0.1', count: 100 }, 'Rate limit warning');
60
57
  expect(true).toBe(true);
61
58
  });
62
59
 
63
- test('记录对象', async () => {
64
- await Logger.log('info', { userId: 1, action: 'test' });
60
+ test('error(obj, msg)', () => {
61
+ const err = new Error('Something went wrong');
62
+ Logger.error({ err: err }, 'Request failed');
65
63
  expect(true).toBe(true);
66
64
  });
67
65
 
68
- test('记录数组', async () => {
69
- await Logger.log('info', ['item1', 'item2']);
66
+ test('debug(obj, msg)', () => {
67
+ Logger.debug({ key: 'value', nested: { a: 1 } }, 'Debug data');
70
68
  expect(true).toBe(true);
71
69
  });
72
70
  });
73
71
 
74
- describe('Logger - 便捷方法', () => {
75
- test('info 方法', () => {
76
- Logger.info('Info test');
72
+ describe('Logger - 仅对象', () => {
73
+ test('info(obj)', () => {
74
+ Logger.info({ event: 'startup', port: 3000 });
77
75
  expect(true).toBe(true);
78
76
  });
79
77
 
80
- test('warn 方法', () => {
81
- Logger.warn('Warn test');
78
+ test('warn(obj)', () => {
79
+ Logger.warn({ type: 'deprecation', feature: 'oldApi' });
82
80
  expect(true).toBe(true);
83
81
  });
84
82
 
85
- test('error 方法', () => {
86
- Logger.error('Error test');
83
+ test('error(obj)', () => {
84
+ Logger.error({ code: 500, message: 'Internal error' });
87
85
  expect(true).toBe(true);
88
86
  });
89
87
 
90
- test('debug 方法', () => {
91
- Logger.debug('Debug test');
88
+ test('debug(obj)', () => {
89
+ Logger.debug({ query: 'SELECT * FROM users', duration: 15 });
92
90
  expect(true).toBe(true);
93
91
  });
94
92
  });
@@ -137,6 +137,47 @@ describe('RedisHelper - Set 操作', () => {
137
137
 
138
138
  await redis.del('test:set:members');
139
139
  });
140
+
141
+ test('saddBatch - 批量向多个 Set 添加成员', async () => {
142
+ const count = await redis.saddBatch([
143
+ { key: 'test:saddBatch:1', members: ['a', 'b'] },
144
+ { key: 'test:saddBatch:2', members: ['c', 'd', 'e'] }
145
+ ]);
146
+ expect(count).toBe(5);
147
+
148
+ // 验证
149
+ const members1 = await redis.smembers('test:saddBatch:1');
150
+ const members2 = await redis.smembers('test:saddBatch:2');
151
+ expect(members1.length).toBe(2);
152
+ expect(members2.length).toBe(3);
153
+
154
+ // 清理
155
+ await redis.delBatch(['test:saddBatch:1', 'test:saddBatch:2']);
156
+ });
157
+
158
+ test('saddBatch - 空数组返回 0', async () => {
159
+ const count = await redis.saddBatch([]);
160
+ expect(count).toBe(0);
161
+ });
162
+
163
+ test('sismemberBatch - 批量检查成员是否存在', async () => {
164
+ await redis.sadd('test:sismemberBatch', ['a', 'b', 'c']);
165
+
166
+ const results = await redis.sismemberBatch([
167
+ { key: 'test:sismemberBatch', member: 'a' },
168
+ { key: 'test:sismemberBatch', member: 'b' },
169
+ { key: 'test:sismemberBatch', member: 'x' }
170
+ ]);
171
+ expect(results).toEqual([true, true, false]);
172
+
173
+ // 清理
174
+ await redis.del('test:sismemberBatch');
175
+ });
176
+
177
+ test('sismemberBatch - 空数组返回空数组', async () => {
178
+ const results = await redis.sismemberBatch([]);
179
+ expect(results).toEqual([]);
180
+ });
140
181
  });
141
182
 
142
183
  describe('RedisHelper - 键操作', () => {
@@ -173,6 +214,38 @@ describe('RedisHelper - 键操作', () => {
173
214
  await redis.del('test:ttl:check');
174
215
  });
175
216
 
217
+ test('ttlBatch - 批量获取剩余过期时间', async () => {
218
+ await redis.setString('test:ttlBatch:1', 'value1', 60);
219
+ await redis.setString('test:ttlBatch:2', 'value2', 120);
220
+ // test:ttlBatch:3 不存在
221
+
222
+ const results = await redis.ttlBatch(['test:ttlBatch:1', 'test:ttlBatch:2', 'test:ttlBatch:3']);
223
+ expect(results.length).toBe(3);
224
+ expect(results[0]).toBeGreaterThan(0);
225
+ expect(results[0]).toBeLessThanOrEqual(60);
226
+ expect(results[1]).toBeGreaterThan(0);
227
+ expect(results[1]).toBeLessThanOrEqual(120);
228
+ expect(results[2]).toBe(-2); // 键不存在
229
+
230
+ // 清理
231
+ await redis.delBatch(['test:ttlBatch:1', 'test:ttlBatch:2']);
232
+ });
233
+
234
+ test('ttlBatch - 空数组返回空数组', async () => {
235
+ const results = await redis.ttlBatch([]);
236
+ expect(results).toEqual([]);
237
+ });
238
+
239
+ test('ttlBatch - 无过期时间返回 -1', async () => {
240
+ await redis.setString('test:ttlBatch:nox', 'value'); // 无 TTL
241
+
242
+ const results = await redis.ttlBatch(['test:ttlBatch:nox']);
243
+ expect(results[0]).toBe(-1);
244
+
245
+ // 清理
246
+ await redis.del('test:ttlBatch:nox');
247
+ });
248
+
176
249
  test('del - 删除键', async () => {
177
250
  await redis.setString('test:delete:key', 'value');
178
251
 
@@ -182,6 +255,203 @@ describe('RedisHelper - 键操作', () => {
182
255
  const value = await redis.getString('test:delete:key');
183
256
  expect(value).toBeNull();
184
257
  });
258
+
259
+ test('delBatch - 批量删除键', async () => {
260
+ // 创建多个键
261
+ await redis.setString('test:batch:1', 'value1');
262
+ await redis.setString('test:batch:2', 'value2');
263
+ await redis.setString('test:batch:3', 'value3');
264
+
265
+ // 批量删除
266
+ const count = await redis.delBatch(['test:batch:1', 'test:batch:2', 'test:batch:3']);
267
+ expect(count).toBe(3);
268
+
269
+ // 验证删除成功
270
+ const value1 = await redis.getString('test:batch:1');
271
+ const value2 = await redis.getString('test:batch:2');
272
+ const value3 = await redis.getString('test:batch:3');
273
+ expect(value1).toBeNull();
274
+ expect(value2).toBeNull();
275
+ expect(value3).toBeNull();
276
+ });
277
+
278
+ test('delBatch - 空数组返回 0', async () => {
279
+ const count = await redis.delBatch([]);
280
+ expect(count).toBe(0);
281
+ });
282
+
283
+ test('delBatch - 删除不存在的键返回 0', async () => {
284
+ const count = await redis.delBatch(['test:non:existent:1', 'test:non:existent:2']);
285
+ expect(count).toBe(0);
286
+ });
287
+
288
+ test('delBatch - 部分存在的键', async () => {
289
+ await redis.setString('test:partial:1', 'value1');
290
+ await redis.setString('test:partial:2', 'value2');
291
+ // test:partial:3 不存在
292
+
293
+ const count = await redis.delBatch(['test:partial:1', 'test:partial:2', 'test:partial:3']);
294
+ expect(count).toBe(2); // 只有 2 个键被删除
295
+ });
296
+
297
+ test('setBatch - 批量设置对象', async () => {
298
+ const items = [
299
+ { key: 'test:setBatch:1', value: { name: 'Alice' } },
300
+ { key: 'test:setBatch:2', value: { name: 'Bob' } },
301
+ { key: 'test:setBatch:3', value: { name: 'Charlie' } }
302
+ ];
303
+
304
+ const count = await redis.setBatch(items);
305
+ expect(count).toBe(3);
306
+
307
+ // 验证设置成功
308
+ const value1 = await redis.getObject('test:setBatch:1');
309
+ const value2 = await redis.getObject('test:setBatch:2');
310
+ const value3 = await redis.getObject('test:setBatch:3');
311
+ expect(value1).toEqual({ name: 'Alice' });
312
+ expect(value2).toEqual({ name: 'Bob' });
313
+ expect(value3).toEqual({ name: 'Charlie' });
314
+
315
+ // 清理
316
+ await redis.delBatch(['test:setBatch:1', 'test:setBatch:2', 'test:setBatch:3']);
317
+ });
318
+
319
+ test('setBatch - 空数组返回 0', async () => {
320
+ const count = await redis.setBatch([]);
321
+ expect(count).toBe(0);
322
+ });
323
+
324
+ test('setBatch - 带 TTL 的批量设置', async () => {
325
+ const items = [
326
+ { key: 'test:setBatch:ttl:1', value: { data: 1 }, ttl: 10 },
327
+ { key: 'test:setBatch:ttl:2', value: { data: 2 }, ttl: 10 }
328
+ ];
329
+
330
+ const count = await redis.setBatch(items);
331
+ expect(count).toBe(2);
332
+
333
+ // 验证 TTL 已设置
334
+ const ttl1 = await redis.ttl('test:setBatch:ttl:1');
335
+ const ttl2 = await redis.ttl('test:setBatch:ttl:2');
336
+ expect(ttl1).toBeGreaterThan(0);
337
+ expect(ttl1).toBeLessThanOrEqual(10);
338
+ expect(ttl2).toBeGreaterThan(0);
339
+ expect(ttl2).toBeLessThanOrEqual(10);
340
+
341
+ // 清理
342
+ await redis.delBatch(['test:setBatch:ttl:1', 'test:setBatch:ttl:2']);
343
+ });
344
+
345
+ test('getBatch - 批量获取对象', async () => {
346
+ // 设置测试数据
347
+ await redis.setObject('test:getBatch:1', { name: 'Alice' });
348
+ await redis.setObject('test:getBatch:2', { name: 'Bob' });
349
+ await redis.setObject('test:getBatch:3', { name: 'Charlie' });
350
+
351
+ // 批量获取
352
+ const results = await redis.getBatch(['test:getBatch:1', 'test:getBatch:2', 'test:getBatch:3']);
353
+ expect(results.length).toBe(3);
354
+ expect(results[0]).toEqual({ name: 'Alice' });
355
+ expect(results[1]).toEqual({ name: 'Bob' });
356
+ expect(results[2]).toEqual({ name: 'Charlie' });
357
+
358
+ // 清理
359
+ await redis.delBatch(['test:getBatch:1', 'test:getBatch:2', 'test:getBatch:3']);
360
+ });
361
+
362
+ test('getBatch - 空数组返回空数组', async () => {
363
+ const results = await redis.getBatch([]);
364
+ expect(results).toEqual([]);
365
+ });
366
+
367
+ test('getBatch - 不存在的键返回 null', async () => {
368
+ const results = await redis.getBatch(['test:non:existent:a', 'test:non:existent:b']);
369
+ expect(results).toEqual([null, null]);
370
+ });
371
+
372
+ test('getBatch - 部分存在的键', async () => {
373
+ await redis.setObject('test:partial:a', { data: 'a' });
374
+ // test:partial:b 不存在
375
+
376
+ const results = await redis.getBatch(['test:partial:a', 'test:partial:b']);
377
+ expect(results.length).toBe(2);
378
+ expect(results[0]).toEqual({ data: 'a' });
379
+ expect(results[1]).toBeNull();
380
+
381
+ // 清理
382
+ await redis.del('test:partial:a');
383
+ });
384
+
385
+ test('existsBatch - 批量检查键是否存在', async () => {
386
+ await redis.setString('test:existsBatch:1', 'value1');
387
+ await redis.setString('test:existsBatch:2', 'value2');
388
+ // test:existsBatch:3 不存在
389
+
390
+ const results = await redis.existsBatch(['test:existsBatch:1', 'test:existsBatch:2', 'test:existsBatch:3']);
391
+ expect(results).toEqual([true, true, false]);
392
+
393
+ // 清理
394
+ await redis.delBatch(['test:existsBatch:1', 'test:existsBatch:2']);
395
+ });
396
+
397
+ test('existsBatch - 空数组返回空数组', async () => {
398
+ const results = await redis.existsBatch([]);
399
+ expect(results).toEqual([]);
400
+ });
401
+
402
+ test('existsBatch - 全部不存在', async () => {
403
+ const results = await redis.existsBatch(['test:none:1', 'test:none:2']);
404
+ expect(results).toEqual([false, false]);
405
+ });
406
+
407
+ test('expireBatch - 批量设置过期时间', async () => {
408
+ await redis.setString('test:expireBatch:1', 'value1');
409
+ await redis.setString('test:expireBatch:2', 'value2');
410
+
411
+ const count = await redis.expireBatch([
412
+ { key: 'test:expireBatch:1', seconds: 60 },
413
+ { key: 'test:expireBatch:2', seconds: 120 }
414
+ ]);
415
+ expect(count).toBe(2);
416
+
417
+ // 验证 TTL 已设置
418
+ const ttl1 = await redis.ttl('test:expireBatch:1');
419
+ const ttl2 = await redis.ttl('test:expireBatch:2');
420
+ expect(ttl1).toBeGreaterThan(0);
421
+ expect(ttl1).toBeLessThanOrEqual(60);
422
+ expect(ttl2).toBeGreaterThan(0);
423
+ expect(ttl2).toBeLessThanOrEqual(120);
424
+
425
+ // 清理
426
+ await redis.delBatch(['test:expireBatch:1', 'test:expireBatch:2']);
427
+ });
428
+
429
+ test('expireBatch - 空数组返回 0', async () => {
430
+ const count = await redis.expireBatch([]);
431
+ expect(count).toBe(0);
432
+ });
433
+
434
+ test('expireBatch - 不存在的键返回 0', async () => {
435
+ const count = await redis.expireBatch([
436
+ { key: 'test:expire:none:1', seconds: 60 },
437
+ { key: 'test:expire:none:2', seconds: 60 }
438
+ ]);
439
+ expect(count).toBe(0);
440
+ });
441
+
442
+ test('expireBatch - 部分存在的键', async () => {
443
+ await redis.setString('test:expire:partial:1', 'value1');
444
+ // test:expire:partial:2 不存在
445
+
446
+ const count = await redis.expireBatch([
447
+ { key: 'test:expire:partial:1', seconds: 60 },
448
+ { key: 'test:expire:partial:2', seconds: 60 }
449
+ ]);
450
+ expect(count).toBe(1);
451
+
452
+ // 清理
453
+ await redis.del('test:expire:partial:1');
454
+ });
185
455
  });
186
456
 
187
457
  describe('RedisHelper - ID 生成', () => {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * RedisKeys 和 RedisTTL 测试
3
+ */
4
+
5
+ import { describe, expect, test } from 'bun:test';
6
+ import { RedisKeys, RedisTTL } from 'befly-shared/redisKeys';
7
+
8
+ describe('RedisKeys - Key 生成函数', () => {
9
+ test('apisAll - 返回固定的接口缓存键', () => {
10
+ expect(RedisKeys.apisAll()).toBe('befly:apis:all');
11
+ });
12
+
13
+ test('menusAll - 返回固定的菜单缓存键', () => {
14
+ expect(RedisKeys.menusAll()).toBe('befly:menus:all');
15
+ });
16
+
17
+ test('roleInfo - 返回带角色代码的缓存键', () => {
18
+ expect(RedisKeys.roleInfo('admin')).toBe('befly:role:info:admin');
19
+ expect(RedisKeys.roleInfo('user')).toBe('befly:role:info:user');
20
+ });
21
+
22
+ test('roleApis - 返回带角色代码的权限缓存键', () => {
23
+ expect(RedisKeys.roleApis('admin')).toBe('befly:role:apis:admin');
24
+ expect(RedisKeys.roleApis('guest')).toBe('befly:role:apis:guest');
25
+ });
26
+
27
+ test('tableColumns - 返回带表名的结构缓存键', () => {
28
+ expect(RedisKeys.tableColumns('user')).toBe('befly:table:columns:user');
29
+ expect(RedisKeys.tableColumns('addon_admin_role')).toBe('befly:table:columns:addon_admin_role');
30
+ });
31
+
32
+ test('roleInfo - 特殊字符处理', () => {
33
+ expect(RedisKeys.roleInfo('super-admin')).toBe('befly:role:info:super-admin');
34
+ expect(RedisKeys.roleInfo('role_1')).toBe('befly:role:info:role_1');
35
+ });
36
+
37
+ test('tableColumns - 空字符串', () => {
38
+ expect(RedisKeys.tableColumns('')).toBe('befly:table:columns:');
39
+ });
40
+ });
41
+
42
+ describe('RedisTTL - 过期时间常量', () => {
43
+ test('tableColumns - 1小时 (3600秒)', () => {
44
+ expect(RedisTTL.tableColumns).toBe(3600);
45
+ });
46
+
47
+ test('roleApis - 24小时 (86400秒)', () => {
48
+ expect(RedisTTL.roleApis).toBe(86400);
49
+ });
50
+
51
+ test('roleInfo - 24小时 (86400秒)', () => {
52
+ expect(RedisTTL.roleInfo).toBe(86400);
53
+ });
54
+
55
+ test('apisAll - 永久 (null)', () => {
56
+ expect(RedisTTL.apisAll).toBeNull();
57
+ });
58
+
59
+ test('menusAll - 永久 (null)', () => {
60
+ expect(RedisTTL.menusAll).toBeNull();
61
+ });
62
+
63
+ test('所有 TTL 值都是数字或 null', () => {
64
+ for (const [key, value] of Object.entries(RedisTTL)) {
65
+ expect(value === null || typeof value === 'number').toBe(true);
66
+ }
67
+ });
68
+
69
+ test('数字类型的 TTL 都是正数', () => {
70
+ for (const [key, value] of Object.entries(RedisTTL)) {
71
+ if (typeof value === 'number') {
72
+ expect(value).toBeGreaterThan(0);
73
+ }
74
+ }
75
+ });
76
+ });
@@ -47,12 +47,6 @@ describe('sync 模块连接管理', () => {
47
47
  });
48
48
  });
49
49
 
50
- describe('Connect.getDbHelper', () => {
51
- test('未连接时应该抛出错误', () => {
52
- expect(() => Connect.getDbHelper()).toThrow('SQL 客户端未连接');
53
- });
54
- });
55
-
56
50
  describe('Mock 连接测试', () => {
57
51
  test('__setMockSql 应该设置 mock SQL 客户端', () => {
58
52
  const mockSql = { close: async () => {} } as any;
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { describe, test, expect, beforeAll } from 'bun:test';
9
+ import { setDbType } from '../sync/syncDb/constants.js';
9
10
 
10
- // 设置环境变量模拟 MySQL 环境
11
- process.env.DB_TYPE = 'mysql';
11
+ // 设置数据库类型为 MySQL
12
+ setDbType('mysql');
12
13
 
13
14
  let compareFieldDefinition: any;
14
15