befly 3.8.29 → 3.8.31
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.
- package/README.md +91 -6
- package/checks/checkApi.ts +2 -1
- package/checks/checkApp.ts +31 -1
- package/checks/checkTable.ts +3 -2
- package/hooks/cors.ts +3 -3
- package/hooks/parser.ts +8 -6
- package/hooks/permission.ts +12 -5
- package/hooks/validator.ts +1 -1
- package/lib/cacheHelper.ts +73 -65
- package/lib/cipher.ts +2 -1
- package/lib/connect.ts +23 -52
- package/lib/dbHelper.ts +14 -11
- package/lib/jwt.ts +58 -437
- package/lib/logger.ts +76 -197
- package/lib/redisHelper.ts +163 -1
- package/lib/sqlBuilder.ts +2 -1
- package/lib/validator.ts +150 -384
- package/loader/loadApis.ts +4 -7
- package/loader/loadHooks.ts +6 -5
- package/loader/loadPlugins.ts +11 -13
- package/main.ts +26 -53
- package/package.json +10 -8
- package/paths.ts +0 -6
- package/plugins/cipher.ts +1 -1
- package/plugins/config.ts +3 -4
- package/plugins/db.ts +6 -7
- package/plugins/jwt.ts +7 -6
- package/plugins/logger.ts +6 -6
- package/plugins/redis.ts +9 -13
- package/router/api.ts +2 -2
- package/router/static.ts +4 -8
- package/sync/syncAll.ts +8 -13
- package/sync/syncApi.ts +14 -10
- package/sync/syncDb/apply.ts +1 -2
- package/sync/syncDb.ts +12 -15
- package/sync/syncDev.ts +19 -56
- package/sync/syncMenu.ts +182 -137
- package/tests/cacheHelper.test.ts +327 -0
- package/tests/dbHelper-columns.test.ts +5 -20
- package/tests/dbHelper-execute.test.ts +14 -68
- package/tests/fields-redis-cache.test.ts +5 -3
- package/tests/integration.test.ts +17 -32
- package/tests/jwt.test.ts +36 -94
- package/tests/logger.test.ts +32 -34
- package/tests/redisHelper.test.ts +271 -2
- package/tests/redisKeys.test.ts +76 -0
- package/tests/sync-connection.test.ts +0 -6
- package/tests/syncDb-constants.test.ts +12 -12
- package/tests/util.test.ts +5 -1
- package/tests/validator.test.ts +611 -85
- package/types/befly.d.ts +9 -15
- package/types/cache.d.ts +73 -0
- package/types/common.d.ts +10 -128
- package/types/database.d.ts +221 -5
- package/types/index.ts +6 -5
- package/types/plugin.d.ts +1 -4
- package/types/redis.d.ts +37 -2
- package/types/table.d.ts +175 -0
- package/config.ts +0 -70
- package/hooks/_rateLimit.ts +0 -64
- package/lib/regexAliases.ts +0 -59
- package/lib/xml.ts +0 -383
- package/tests/validator-advanced.test.ts +0 -653
- package/tests/xml.test.ts +0 -101
- package/types/addon.d.ts +0 -50
- package/types/crypto.d.ts +0 -23
- package/types/jwt.d.ts +0 -99
- package/types/logger.d.ts +0 -43
- package/types/tool.d.ts +0 -67
- package/types/validator.d.ts +0 -43
package/tests/jwt.test.ts
CHANGED
|
@@ -1,122 +1,64 @@
|
|
|
1
|
-
import { describe, test, expect
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import { Jwt } from '../lib/jwt';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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',
|
|
14
|
-
const token =
|
|
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('
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
expect(decoded.
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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('
|
|
32
|
-
const token =
|
|
33
|
-
|
|
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('
|
|
38
|
-
const
|
|
39
|
-
const
|
|
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('密钥不匹配验证失败',
|
|
45
|
-
const token =
|
|
46
|
-
expect(
|
|
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('解码不验证签名',
|
|
52
|
-
const token =
|
|
53
|
-
const decoded =
|
|
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('解码完整信息',
|
|
58
|
-
const token =
|
|
59
|
-
const decoded =
|
|
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
|
-
});
|
package/tests/logger.test.ts
CHANGED
|
@@ -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('
|
|
27
|
-
|
|
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('
|
|
32
|
-
|
|
33
|
+
test('warn(msg)', () => {
|
|
34
|
+
Logger.warn('Test warning');
|
|
33
35
|
expect(true).toBe(true);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
|
-
test('
|
|
37
|
-
|
|
38
|
+
test('error(msg)', () => {
|
|
39
|
+
Logger.error('Test error');
|
|
38
40
|
expect(true).toBe(true);
|
|
39
41
|
});
|
|
40
42
|
|
|
41
|
-
test('
|
|
42
|
-
|
|
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('
|
|
54
|
-
|
|
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('
|
|
59
|
-
|
|
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('
|
|
64
|
-
|
|
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('
|
|
69
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
88
|
+
test('debug(obj)', () => {
|
|
89
|
+
Logger.debug({ query: 'SELECT * FROM users', duration: 15 });
|
|
92
90
|
expect(true).toBe(true);
|
|
93
91
|
});
|
|
94
92
|
});
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { describe, expect, it, test, beforeAll, afterAll } from 'bun:test';
|
|
7
7
|
import { RedisClient } from 'bun';
|
|
8
8
|
|
|
9
|
-
import { defaultOptions } from '../config.js';
|
|
10
9
|
import { Connect } from '../lib/connect.js';
|
|
11
10
|
import { RedisHelper } from '../lib/redisHelper.js';
|
|
12
11
|
|
|
@@ -14,7 +13,7 @@ let redis: RedisHelper;
|
|
|
14
13
|
|
|
15
14
|
beforeAll(async () => {
|
|
16
15
|
// 连接 Redis
|
|
17
|
-
await Connect.connectRedis(
|
|
16
|
+
await Connect.connectRedis();
|
|
18
17
|
redis = new RedisHelper();
|
|
19
18
|
});
|
|
20
19
|
|
|
@@ -137,6 +136,47 @@ describe('RedisHelper - Set 操作', () => {
|
|
|
137
136
|
|
|
138
137
|
await redis.del('test:set:members');
|
|
139
138
|
});
|
|
139
|
+
|
|
140
|
+
test('saddBatch - 批量向多个 Set 添加成员', async () => {
|
|
141
|
+
const count = await redis.saddBatch([
|
|
142
|
+
{ key: 'test:saddBatch:1', members: ['a', 'b'] },
|
|
143
|
+
{ key: 'test:saddBatch:2', members: ['c', 'd', 'e'] }
|
|
144
|
+
]);
|
|
145
|
+
expect(count).toBe(5);
|
|
146
|
+
|
|
147
|
+
// 验证
|
|
148
|
+
const members1 = await redis.smembers('test:saddBatch:1');
|
|
149
|
+
const members2 = await redis.smembers('test:saddBatch:2');
|
|
150
|
+
expect(members1.length).toBe(2);
|
|
151
|
+
expect(members2.length).toBe(3);
|
|
152
|
+
|
|
153
|
+
// 清理
|
|
154
|
+
await redis.delBatch(['test:saddBatch:1', 'test:saddBatch:2']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('saddBatch - 空数组返回 0', async () => {
|
|
158
|
+
const count = await redis.saddBatch([]);
|
|
159
|
+
expect(count).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('sismemberBatch - 批量检查成员是否存在', async () => {
|
|
163
|
+
await redis.sadd('test:sismemberBatch', ['a', 'b', 'c']);
|
|
164
|
+
|
|
165
|
+
const results = await redis.sismemberBatch([
|
|
166
|
+
{ key: 'test:sismemberBatch', member: 'a' },
|
|
167
|
+
{ key: 'test:sismemberBatch', member: 'b' },
|
|
168
|
+
{ key: 'test:sismemberBatch', member: 'x' }
|
|
169
|
+
]);
|
|
170
|
+
expect(results).toEqual([true, true, false]);
|
|
171
|
+
|
|
172
|
+
// 清理
|
|
173
|
+
await redis.del('test:sismemberBatch');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('sismemberBatch - 空数组返回空数组', async () => {
|
|
177
|
+
const results = await redis.sismemberBatch([]);
|
|
178
|
+
expect(results).toEqual([]);
|
|
179
|
+
});
|
|
140
180
|
});
|
|
141
181
|
|
|
142
182
|
describe('RedisHelper - 键操作', () => {
|
|
@@ -173,6 +213,38 @@ describe('RedisHelper - 键操作', () => {
|
|
|
173
213
|
await redis.del('test:ttl:check');
|
|
174
214
|
});
|
|
175
215
|
|
|
216
|
+
test('ttlBatch - 批量获取剩余过期时间', async () => {
|
|
217
|
+
await redis.setString('test:ttlBatch:1', 'value1', 60);
|
|
218
|
+
await redis.setString('test:ttlBatch:2', 'value2', 120);
|
|
219
|
+
// test:ttlBatch:3 不存在
|
|
220
|
+
|
|
221
|
+
const results = await redis.ttlBatch(['test:ttlBatch:1', 'test:ttlBatch:2', 'test:ttlBatch:3']);
|
|
222
|
+
expect(results.length).toBe(3);
|
|
223
|
+
expect(results[0]).toBeGreaterThan(0);
|
|
224
|
+
expect(results[0]).toBeLessThanOrEqual(60);
|
|
225
|
+
expect(results[1]).toBeGreaterThan(0);
|
|
226
|
+
expect(results[1]).toBeLessThanOrEqual(120);
|
|
227
|
+
expect(results[2]).toBe(-2); // 键不存在
|
|
228
|
+
|
|
229
|
+
// 清理
|
|
230
|
+
await redis.delBatch(['test:ttlBatch:1', 'test:ttlBatch:2']);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('ttlBatch - 空数组返回空数组', async () => {
|
|
234
|
+
const results = await redis.ttlBatch([]);
|
|
235
|
+
expect(results).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('ttlBatch - 无过期时间返回 -1', async () => {
|
|
239
|
+
await redis.setString('test:ttlBatch:nox', 'value'); // 无 TTL
|
|
240
|
+
|
|
241
|
+
const results = await redis.ttlBatch(['test:ttlBatch:nox']);
|
|
242
|
+
expect(results[0]).toBe(-1);
|
|
243
|
+
|
|
244
|
+
// 清理
|
|
245
|
+
await redis.del('test:ttlBatch:nox');
|
|
246
|
+
});
|
|
247
|
+
|
|
176
248
|
test('del - 删除键', async () => {
|
|
177
249
|
await redis.setString('test:delete:key', 'value');
|
|
178
250
|
|
|
@@ -182,6 +254,203 @@ describe('RedisHelper - 键操作', () => {
|
|
|
182
254
|
const value = await redis.getString('test:delete:key');
|
|
183
255
|
expect(value).toBeNull();
|
|
184
256
|
});
|
|
257
|
+
|
|
258
|
+
test('delBatch - 批量删除键', async () => {
|
|
259
|
+
// 创建多个键
|
|
260
|
+
await redis.setString('test:batch:1', 'value1');
|
|
261
|
+
await redis.setString('test:batch:2', 'value2');
|
|
262
|
+
await redis.setString('test:batch:3', 'value3');
|
|
263
|
+
|
|
264
|
+
// 批量删除
|
|
265
|
+
const count = await redis.delBatch(['test:batch:1', 'test:batch:2', 'test:batch:3']);
|
|
266
|
+
expect(count).toBe(3);
|
|
267
|
+
|
|
268
|
+
// 验证删除成功
|
|
269
|
+
const value1 = await redis.getString('test:batch:1');
|
|
270
|
+
const value2 = await redis.getString('test:batch:2');
|
|
271
|
+
const value3 = await redis.getString('test:batch:3');
|
|
272
|
+
expect(value1).toBeNull();
|
|
273
|
+
expect(value2).toBeNull();
|
|
274
|
+
expect(value3).toBeNull();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('delBatch - 空数组返回 0', async () => {
|
|
278
|
+
const count = await redis.delBatch([]);
|
|
279
|
+
expect(count).toBe(0);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('delBatch - 删除不存在的键返回 0', async () => {
|
|
283
|
+
const count = await redis.delBatch(['test:non:existent:1', 'test:non:existent:2']);
|
|
284
|
+
expect(count).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('delBatch - 部分存在的键', async () => {
|
|
288
|
+
await redis.setString('test:partial:1', 'value1');
|
|
289
|
+
await redis.setString('test:partial:2', 'value2');
|
|
290
|
+
// test:partial:3 不存在
|
|
291
|
+
|
|
292
|
+
const count = await redis.delBatch(['test:partial:1', 'test:partial:2', 'test:partial:3']);
|
|
293
|
+
expect(count).toBe(2); // 只有 2 个键被删除
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('setBatch - 批量设置对象', async () => {
|
|
297
|
+
const items = [
|
|
298
|
+
{ key: 'test:setBatch:1', value: { name: 'Alice' } },
|
|
299
|
+
{ key: 'test:setBatch:2', value: { name: 'Bob' } },
|
|
300
|
+
{ key: 'test:setBatch:3', value: { name: 'Charlie' } }
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
const count = await redis.setBatch(items);
|
|
304
|
+
expect(count).toBe(3);
|
|
305
|
+
|
|
306
|
+
// 验证设置成功
|
|
307
|
+
const value1 = await redis.getObject('test:setBatch:1');
|
|
308
|
+
const value2 = await redis.getObject('test:setBatch:2');
|
|
309
|
+
const value3 = await redis.getObject('test:setBatch:3');
|
|
310
|
+
expect(value1).toEqual({ name: 'Alice' });
|
|
311
|
+
expect(value2).toEqual({ name: 'Bob' });
|
|
312
|
+
expect(value3).toEqual({ name: 'Charlie' });
|
|
313
|
+
|
|
314
|
+
// 清理
|
|
315
|
+
await redis.delBatch(['test:setBatch:1', 'test:setBatch:2', 'test:setBatch:3']);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('setBatch - 空数组返回 0', async () => {
|
|
319
|
+
const count = await redis.setBatch([]);
|
|
320
|
+
expect(count).toBe(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('setBatch - 带 TTL 的批量设置', async () => {
|
|
324
|
+
const items = [
|
|
325
|
+
{ key: 'test:setBatch:ttl:1', value: { data: 1 }, ttl: 10 },
|
|
326
|
+
{ key: 'test:setBatch:ttl:2', value: { data: 2 }, ttl: 10 }
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const count = await redis.setBatch(items);
|
|
330
|
+
expect(count).toBe(2);
|
|
331
|
+
|
|
332
|
+
// 验证 TTL 已设置
|
|
333
|
+
const ttl1 = await redis.ttl('test:setBatch:ttl:1');
|
|
334
|
+
const ttl2 = await redis.ttl('test:setBatch:ttl:2');
|
|
335
|
+
expect(ttl1).toBeGreaterThan(0);
|
|
336
|
+
expect(ttl1).toBeLessThanOrEqual(10);
|
|
337
|
+
expect(ttl2).toBeGreaterThan(0);
|
|
338
|
+
expect(ttl2).toBeLessThanOrEqual(10);
|
|
339
|
+
|
|
340
|
+
// 清理
|
|
341
|
+
await redis.delBatch(['test:setBatch:ttl:1', 'test:setBatch:ttl:2']);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('getBatch - 批量获取对象', async () => {
|
|
345
|
+
// 设置测试数据
|
|
346
|
+
await redis.setObject('test:getBatch:1', { name: 'Alice' });
|
|
347
|
+
await redis.setObject('test:getBatch:2', { name: 'Bob' });
|
|
348
|
+
await redis.setObject('test:getBatch:3', { name: 'Charlie' });
|
|
349
|
+
|
|
350
|
+
// 批量获取
|
|
351
|
+
const results = await redis.getBatch(['test:getBatch:1', 'test:getBatch:2', 'test:getBatch:3']);
|
|
352
|
+
expect(results.length).toBe(3);
|
|
353
|
+
expect(results[0]).toEqual({ name: 'Alice' });
|
|
354
|
+
expect(results[1]).toEqual({ name: 'Bob' });
|
|
355
|
+
expect(results[2]).toEqual({ name: 'Charlie' });
|
|
356
|
+
|
|
357
|
+
// 清理
|
|
358
|
+
await redis.delBatch(['test:getBatch:1', 'test:getBatch:2', 'test:getBatch:3']);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('getBatch - 空数组返回空数组', async () => {
|
|
362
|
+
const results = await redis.getBatch([]);
|
|
363
|
+
expect(results).toEqual([]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('getBatch - 不存在的键返回 null', async () => {
|
|
367
|
+
const results = await redis.getBatch(['test:non:existent:a', 'test:non:existent:b']);
|
|
368
|
+
expect(results).toEqual([null, null]);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('getBatch - 部分存在的键', async () => {
|
|
372
|
+
await redis.setObject('test:partial:a', { data: 'a' });
|
|
373
|
+
// test:partial:b 不存在
|
|
374
|
+
|
|
375
|
+
const results = await redis.getBatch(['test:partial:a', 'test:partial:b']);
|
|
376
|
+
expect(results.length).toBe(2);
|
|
377
|
+
expect(results[0]).toEqual({ data: 'a' });
|
|
378
|
+
expect(results[1]).toBeNull();
|
|
379
|
+
|
|
380
|
+
// 清理
|
|
381
|
+
await redis.del('test:partial:a');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('existsBatch - 批量检查键是否存在', async () => {
|
|
385
|
+
await redis.setString('test:existsBatch:1', 'value1');
|
|
386
|
+
await redis.setString('test:existsBatch:2', 'value2');
|
|
387
|
+
// test:existsBatch:3 不存在
|
|
388
|
+
|
|
389
|
+
const results = await redis.existsBatch(['test:existsBatch:1', 'test:existsBatch:2', 'test:existsBatch:3']);
|
|
390
|
+
expect(results).toEqual([true, true, false]);
|
|
391
|
+
|
|
392
|
+
// 清理
|
|
393
|
+
await redis.delBatch(['test:existsBatch:1', 'test:existsBatch:2']);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('existsBatch - 空数组返回空数组', async () => {
|
|
397
|
+
const results = await redis.existsBatch([]);
|
|
398
|
+
expect(results).toEqual([]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('existsBatch - 全部不存在', async () => {
|
|
402
|
+
const results = await redis.existsBatch(['test:none:1', 'test:none:2']);
|
|
403
|
+
expect(results).toEqual([false, false]);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('expireBatch - 批量设置过期时间', async () => {
|
|
407
|
+
await redis.setString('test:expireBatch:1', 'value1');
|
|
408
|
+
await redis.setString('test:expireBatch:2', 'value2');
|
|
409
|
+
|
|
410
|
+
const count = await redis.expireBatch([
|
|
411
|
+
{ key: 'test:expireBatch:1', seconds: 60 },
|
|
412
|
+
{ key: 'test:expireBatch:2', seconds: 120 }
|
|
413
|
+
]);
|
|
414
|
+
expect(count).toBe(2);
|
|
415
|
+
|
|
416
|
+
// 验证 TTL 已设置
|
|
417
|
+
const ttl1 = await redis.ttl('test:expireBatch:1');
|
|
418
|
+
const ttl2 = await redis.ttl('test:expireBatch:2');
|
|
419
|
+
expect(ttl1).toBeGreaterThan(0);
|
|
420
|
+
expect(ttl1).toBeLessThanOrEqual(60);
|
|
421
|
+
expect(ttl2).toBeGreaterThan(0);
|
|
422
|
+
expect(ttl2).toBeLessThanOrEqual(120);
|
|
423
|
+
|
|
424
|
+
// 清理
|
|
425
|
+
await redis.delBatch(['test:expireBatch:1', 'test:expireBatch:2']);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('expireBatch - 空数组返回 0', async () => {
|
|
429
|
+
const count = await redis.expireBatch([]);
|
|
430
|
+
expect(count).toBe(0);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('expireBatch - 不存在的键返回 0', async () => {
|
|
434
|
+
const count = await redis.expireBatch([
|
|
435
|
+
{ key: 'test:expire:none:1', seconds: 60 },
|
|
436
|
+
{ key: 'test:expire:none:2', seconds: 60 }
|
|
437
|
+
]);
|
|
438
|
+
expect(count).toBe(0);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('expireBatch - 部分存在的键', async () => {
|
|
442
|
+
await redis.setString('test:expire:partial:1', 'value1');
|
|
443
|
+
// test:expire:partial:2 不存在
|
|
444
|
+
|
|
445
|
+
const count = await redis.expireBatch([
|
|
446
|
+
{ key: 'test:expire:partial:1', seconds: 60 },
|
|
447
|
+
{ key: 'test:expire:partial:2', seconds: 60 }
|
|
448
|
+
]);
|
|
449
|
+
expect(count).toBe(1);
|
|
450
|
+
|
|
451
|
+
// 清理
|
|
452
|
+
await redis.del('test:expire:partial:1');
|
|
453
|
+
});
|
|
185
454
|
});
|
|
186
455
|
|
|
187
456
|
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;
|