befly 3.8.20 → 3.8.24
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/checks/checkApi.ts +92 -0
- package/checks/checkApp.ts +31 -0
- package/checks/checkTable.ts +247 -0
- package/config.ts +71 -0
- package/hooks/auth.ts +30 -0
- package/hooks/cors.ts +48 -0
- package/hooks/errorHandler.ts +23 -0
- package/hooks/parser.ts +67 -0
- package/hooks/permission.ts +54 -0
- package/hooks/rateLimit.ts +70 -0
- package/hooks/requestId.ts +24 -0
- package/hooks/requestLogger.ts +25 -0
- package/hooks/responseFormatter.ts +64 -0
- package/hooks/validator.ts +34 -0
- package/package.json +15 -14
- package/tests/cipher.test.ts +248 -0
- package/tests/dbHelper-advanced.test.ts +717 -0
- package/tests/dbHelper-columns.test.ts +266 -0
- package/tests/dbHelper-execute.test.ts +240 -0
- package/tests/fields-redis-cache.test.ts +123 -0
- package/tests/fields-validate.test.ts +99 -0
- package/tests/integration.test.ts +202 -0
- package/tests/jwt.test.ts +122 -0
- package/tests/logger.test.ts +94 -0
- package/tests/redisHelper.test.ts +231 -0
- package/tests/sqlBuilder-advanced.test.ts +593 -0
- package/tests/sqlBuilder.test.ts +184 -0
- package/tests/util.test.ts +95 -0
- package/tests/validator-advanced.test.ts +653 -0
- package/tests/validator.test.ts +148 -0
- package/tests/xml.test.ts +101 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DbHelper 高级测试用例
|
|
3
|
+
* 测试边界条件、错误处理、复杂场景
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
describe('DbHelper - 字段验证逻辑', () => {
|
|
9
|
+
test('validateAndClassifyFields - 空数组应返回 all 类型', () => {
|
|
10
|
+
// 测试目的:验证空数组被正确识别为查询所有字段
|
|
11
|
+
const mockHelper = {
|
|
12
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
13
|
+
if (!fields || fields.length === 0) {
|
|
14
|
+
return { type: 'all' as const, fields: [] };
|
|
15
|
+
}
|
|
16
|
+
return { type: 'include' as const, fields: fields };
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const result = mockHelper.validateAndClassifyFields([]);
|
|
21
|
+
expect(result.type).toBe('all');
|
|
22
|
+
expect(result.fields.length).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('validateAndClassifyFields - undefined 应返回 all 类型', () => {
|
|
26
|
+
const mockHelper = {
|
|
27
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
28
|
+
if (!fields || fields.length === 0) {
|
|
29
|
+
return { type: 'all' as const, fields: [] };
|
|
30
|
+
}
|
|
31
|
+
return { type: 'include' as const, fields: fields };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = mockHelper.validateAndClassifyFields(undefined);
|
|
36
|
+
expect(result.type).toBe('all');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('validateAndClassifyFields - 星号应抛出错误', () => {
|
|
40
|
+
const mockHelper = {
|
|
41
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
42
|
+
if (!fields || fields.length === 0) {
|
|
43
|
+
return { type: 'all' as const, fields: [] };
|
|
44
|
+
}
|
|
45
|
+
if (fields.some((f) => f === '*')) {
|
|
46
|
+
throw new Error('fields 不支持 * 星号');
|
|
47
|
+
}
|
|
48
|
+
return { type: 'include' as const, fields: fields };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(() => {
|
|
53
|
+
mockHelper.validateAndClassifyFields(['id', '*', 'name']);
|
|
54
|
+
}).toThrow('fields 不支持 * 星号');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('validateAndClassifyFields - 空字符串应抛出错误', () => {
|
|
58
|
+
const mockHelper = {
|
|
59
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
60
|
+
if (fields.some((f) => !f || typeof f !== 'string' || f.trim() === '')) {
|
|
61
|
+
throw new Error('fields 不能包含空字符串或无效值');
|
|
62
|
+
}
|
|
63
|
+
return { type: 'include' as const, fields: fields };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(() => {
|
|
68
|
+
mockHelper.validateAndClassifyFields(['id', '', 'name']);
|
|
69
|
+
}).toThrow('fields 不能包含空字符串或无效值');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('validateAndClassifyFields - 混用包含和排除字段应抛出错误', () => {
|
|
73
|
+
const mockHelper = {
|
|
74
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
75
|
+
const includeFields = fields!.filter((f) => !f.startsWith('!'));
|
|
76
|
+
const excludeFields = fields!.filter((f) => f.startsWith('!'));
|
|
77
|
+
|
|
78
|
+
if (includeFields.length > 0 && excludeFields.length > 0) {
|
|
79
|
+
throw new Error('fields 不能同时包含普通字段和排除字段');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (excludeFields.length > 0) {
|
|
83
|
+
return { type: 'exclude' as const, fields: excludeFields.map((f) => f.substring(1)) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { type: 'include' as const, fields: includeFields };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
expect(() => {
|
|
91
|
+
mockHelper.validateAndClassifyFields(['id', '!password', 'name']);
|
|
92
|
+
}).toThrow('fields 不能同时包含普通字段和排除字段');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('validateAndClassifyFields - 排除字段应正确去除感叹号', () => {
|
|
96
|
+
const mockHelper = {
|
|
97
|
+
validateAndClassifyFields: (fields?: string[]) => {
|
|
98
|
+
const excludeFields = fields!.filter((f) => f.startsWith('!'));
|
|
99
|
+
return { type: 'exclude' as const, fields: excludeFields.map((f) => f.substring(1)) };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const result = mockHelper.validateAndClassifyFields(['!password', '!token', '!salt']);
|
|
104
|
+
expect(result.type).toBe('exclude');
|
|
105
|
+
expect(result.fields).toEqual(['password', 'token', 'salt']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
110
|
+
test('convertBigIntFields - 白名单字段应转换', () => {
|
|
111
|
+
const mockConvert = (arr: any[]) => {
|
|
112
|
+
return arr.map((item) => {
|
|
113
|
+
const converted = { ...item };
|
|
114
|
+
const whiteList = ['id', 'pid', 'sort'];
|
|
115
|
+
for (const key of whiteList) {
|
|
116
|
+
if (key in converted && typeof converted[key] === 'string') {
|
|
117
|
+
converted[key] = Number(converted[key]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return converted;
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const data = [{ id: '123', pid: '456', sort: '10', name: 'test' }];
|
|
125
|
+
const result = mockConvert(data);
|
|
126
|
+
|
|
127
|
+
expect(result[0].id).toBe(123);
|
|
128
|
+
expect(result[0].pid).toBe(456);
|
|
129
|
+
expect(result[0].sort).toBe(10);
|
|
130
|
+
expect(typeof result[0].id).toBe('number');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('convertBigIntFields - Id 后缀字段应转换', () => {
|
|
134
|
+
const mockConvert = (arr: any[]) => {
|
|
135
|
+
return arr.map((item) => {
|
|
136
|
+
const converted = { ...item };
|
|
137
|
+
for (const [key, value] of Object.entries(converted)) {
|
|
138
|
+
if ((key.endsWith('Id') || key.endsWith('_id')) && typeof value === 'string') {
|
|
139
|
+
converted[key] = Number(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return converted;
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const data = [{ userId: '100', roleId: '200', category_id: '300', name: 'test' }];
|
|
147
|
+
const result = mockConvert(data);
|
|
148
|
+
|
|
149
|
+
expect(result[0].userId).toBe(100);
|
|
150
|
+
expect(result[0].roleId).toBe(200);
|
|
151
|
+
expect(result[0].category_id).toBe(300);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('convertBigIntFields - At 后缀字段应转换(时间戳)', () => {
|
|
155
|
+
const mockConvert = (arr: any[]) => {
|
|
156
|
+
return arr.map((item) => {
|
|
157
|
+
const converted = { ...item };
|
|
158
|
+
for (const [key, value] of Object.entries(converted)) {
|
|
159
|
+
if ((key.endsWith('At') || key.endsWith('_at')) && typeof value === 'string') {
|
|
160
|
+
converted[key] = Number(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return converted;
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const data = [{ createdAt: '1609459200000', updatedAt: '1609459300000', deleted_at: '0' }];
|
|
168
|
+
const result = mockConvert(data);
|
|
169
|
+
|
|
170
|
+
expect(result[0].createdAt).toBe(1609459200000);
|
|
171
|
+
expect(result[0].updatedAt).toBe(1609459300000);
|
|
172
|
+
expect(result[0].deleted_at).toBe(0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('convertBigIntFields - 非数字字符串不应转换', () => {
|
|
176
|
+
const mockConvert = (arr: any[]) => {
|
|
177
|
+
return arr.map((item) => {
|
|
178
|
+
const converted = { ...item };
|
|
179
|
+
for (const [key, value] of Object.entries(converted)) {
|
|
180
|
+
if (key.endsWith('Id') && typeof value === 'string') {
|
|
181
|
+
const num = Number(value);
|
|
182
|
+
if (!isNaN(num)) {
|
|
183
|
+
converted[key] = num;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return converted;
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const data = [{ userId: 'invalid', roleId: '123' }];
|
|
192
|
+
const result = mockConvert(data);
|
|
193
|
+
|
|
194
|
+
expect(result[0].userId).toBe('invalid'); // 保持原值
|
|
195
|
+
expect(result[0].roleId).toBe(123);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('convertBigIntFields - null 和 undefined 应跳过', () => {
|
|
199
|
+
const mockConvert = (arr: any[]) => {
|
|
200
|
+
return arr.map((item) => {
|
|
201
|
+
const converted = { ...item };
|
|
202
|
+
for (const [key, value] of Object.entries(converted)) {
|
|
203
|
+
if (value === null || value === undefined) continue;
|
|
204
|
+
if (key.endsWith('Id') && typeof value === 'string') {
|
|
205
|
+
converted[key] = Number(value);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return converted;
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const data = [{ userId: null, roleId: undefined, categoryId: '123' }];
|
|
213
|
+
const result = mockConvert(data);
|
|
214
|
+
|
|
215
|
+
expect(result[0].userId).toBeNull();
|
|
216
|
+
expect(result[0].roleId).toBeUndefined();
|
|
217
|
+
expect(result[0].categoryId).toBe(123);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('convertBigIntFields - 空数组应返回空数组', () => {
|
|
221
|
+
const mockConvert = (arr: any[]) => {
|
|
222
|
+
if (!arr || !Array.isArray(arr)) return arr;
|
|
223
|
+
return arr;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = mockConvert([]);
|
|
227
|
+
expect(result).toEqual([]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('convertBigIntFields - 非数组应返回原值', () => {
|
|
231
|
+
const mockConvert = (arr: any) => {
|
|
232
|
+
if (!arr || !Array.isArray(arr)) return arr;
|
|
233
|
+
return arr;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const result = mockConvert(null);
|
|
237
|
+
expect(result).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('DbHelper - WHERE 条件键名转换', () => {
|
|
242
|
+
test('whereKeysToSnake - 简单字段名应转换为下划线', () => {
|
|
243
|
+
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
244
|
+
|
|
245
|
+
const mockConvert = (where: any): any => {
|
|
246
|
+
const result: any = {};
|
|
247
|
+
for (const [key, value] of Object.entries(where)) {
|
|
248
|
+
result[snakeCase(key)] = value;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const where = { userId: 123, userName: 'john' };
|
|
254
|
+
const result = mockConvert(where);
|
|
255
|
+
|
|
256
|
+
expect(result.user_id).toBe(123);
|
|
257
|
+
expect(result.user_name).toBe('john');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('whereKeysToSnake - 带操作符的字段名应正确处理', () => {
|
|
261
|
+
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
262
|
+
|
|
263
|
+
const mockConvert = (where: any): any => {
|
|
264
|
+
const result: any = {};
|
|
265
|
+
for (const [key, value] of Object.entries(where)) {
|
|
266
|
+
if (key.includes('$')) {
|
|
267
|
+
const lastDollarIndex = key.lastIndexOf('$');
|
|
268
|
+
const fieldName = key.substring(0, lastDollarIndex);
|
|
269
|
+
const operator = key.substring(lastDollarIndex);
|
|
270
|
+
result[snakeCase(fieldName) + operator] = value;
|
|
271
|
+
} else {
|
|
272
|
+
result[snakeCase(key)] = value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const where = { userId$gt: 100, userName$like: '%john%' };
|
|
279
|
+
const result = mockConvert(where);
|
|
280
|
+
|
|
281
|
+
expect(result.user_id$gt).toBe(100);
|
|
282
|
+
expect(result.user_name$like).toBe('%john%');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('whereKeysToSnake - $or 和 $and 应递归处理', () => {
|
|
286
|
+
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
287
|
+
|
|
288
|
+
const mockConvert = (where: any): any => {
|
|
289
|
+
if (!where || typeof where !== 'object') return where;
|
|
290
|
+
if (Array.isArray(where)) {
|
|
291
|
+
return where.map((item) => mockConvert(item));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const result: any = {};
|
|
295
|
+
for (const [key, value] of Object.entries(where)) {
|
|
296
|
+
if (key === '$or' || key === '$and') {
|
|
297
|
+
result[key] = (value as any[]).map((item) => mockConvert(item));
|
|
298
|
+
} else {
|
|
299
|
+
result[snakeCase(key)] = value;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const where = {
|
|
306
|
+
$or: [{ userId: 1 }, { userName: 'john' }]
|
|
307
|
+
};
|
|
308
|
+
const result = mockConvert(where);
|
|
309
|
+
|
|
310
|
+
expect(result.$or[0].user_id).toBe(1);
|
|
311
|
+
expect(result.$or[1].user_name).toBe('john');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('whereKeysToSnake - 嵌套对象应递归转换', () => {
|
|
315
|
+
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
316
|
+
|
|
317
|
+
const mockConvert = (where: any): any => {
|
|
318
|
+
if (!where || typeof where !== 'object') return where;
|
|
319
|
+
|
|
320
|
+
const result: any = {};
|
|
321
|
+
for (const [key, value] of Object.entries(where)) {
|
|
322
|
+
const snakeKey = snakeCase(key);
|
|
323
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
324
|
+
result[snakeKey] = mockConvert(value);
|
|
325
|
+
} else {
|
|
326
|
+
result[snakeKey] = value;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const where = {
|
|
333
|
+
userProfile: {
|
|
334
|
+
firstName: 'John',
|
|
335
|
+
lastName: 'Doe'
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const result = mockConvert(where);
|
|
339
|
+
|
|
340
|
+
expect(result.user_profile.first_name).toBe('John');
|
|
341
|
+
expect(result.user_profile.last_name).toBe('Doe');
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('DbHelper - 默认状态过滤', () => {
|
|
346
|
+
test('addDefaultStateFilter - 应添加 state > 0 条件', () => {
|
|
347
|
+
const mockAddFilter = (where: any) => {
|
|
348
|
+
return {
|
|
349
|
+
...where,
|
|
350
|
+
state$gt: 0
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const where = { userId: 123 };
|
|
355
|
+
const result = mockAddFilter(where);
|
|
356
|
+
|
|
357
|
+
expect(result.userId).toBe(123);
|
|
358
|
+
expect(result.state$gt).toBe(0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('addDefaultStateFilter - 已有 state 条件不应覆盖', () => {
|
|
362
|
+
// **潜在问题**:如果用户传入 state: 0(查询已删除数据),
|
|
363
|
+
// addDefaultStateFilter 可能会添加 state$gt: 0,导致冲突
|
|
364
|
+
|
|
365
|
+
const mockAddFilter = (where: any) => {
|
|
366
|
+
// 正确做法:检查是否已有 state 相关条件
|
|
367
|
+
if ('state' in where || 'state$gt' in where || 'state$gte' in where) {
|
|
368
|
+
return where; // 不添加默认过滤
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...where,
|
|
372
|
+
state$gt: 0
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const where1 = { userId: 123, state: 0 }; // 查询已删除数据
|
|
377
|
+
const result1 = mockAddFilter(where1);
|
|
378
|
+
expect(result1.state).toBe(0);
|
|
379
|
+
expect(result1.state$gt).toBeUndefined(); // 不应添加
|
|
380
|
+
|
|
381
|
+
const where2 = { userId: 123 };
|
|
382
|
+
const result2 = mockAddFilter(where2);
|
|
383
|
+
expect(result2.state$gt).toBe(0); // 应添加
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('DbHelper - 分页参数验证', () => {
|
|
388
|
+
test('getList - page 小于 1 应抛出错误', () => {
|
|
389
|
+
const mockValidate = (page: number, limit: number) => {
|
|
390
|
+
if (page < 1 || page > 10000) {
|
|
391
|
+
throw new Error(`页码必须在 1 到 10000 之间 (page: ${page})`);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
expect(() => mockValidate(0, 10)).toThrow('页码必须在 1 到 10000 之间');
|
|
396
|
+
expect(() => mockValidate(-5, 10)).toThrow('页码必须在 1 到 10000 之间');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('getList - page 大于 10000 应抛出错误', () => {
|
|
400
|
+
const mockValidate = (page: number) => {
|
|
401
|
+
if (page > 10000) {
|
|
402
|
+
throw new Error(`页码必须在 1 到 10000 之间 (page: ${page})`);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
expect(() => mockValidate(10001)).toThrow('页码必须在 1 到 10000 之间');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('getList - limit 小于 1 应抛出错误', () => {
|
|
410
|
+
const mockValidate = (limit: number) => {
|
|
411
|
+
if (limit < 1 || limit > 1000) {
|
|
412
|
+
throw new Error(`每页数量必须在 1 到 1000 之间 (limit: ${limit})`);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
expect(() => mockValidate(0)).toThrow('每页数量必须在 1 到 1000 之间');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('getList - limit 大于 1000 应抛出错误', () => {
|
|
420
|
+
const mockValidate = (limit: number) => {
|
|
421
|
+
if (limit > 1000) {
|
|
422
|
+
throw new Error(`每页数量必须在 1 到 1000 之间 (limit: ${limit})`);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
expect(() => mockValidate(1001)).toThrow('每页数量必须在 1 到 1000 之间');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('getList - 边界值测试', () => {
|
|
430
|
+
const mockValidate = (page: number, limit: number) => {
|
|
431
|
+
if (page < 1 || page > 10000) {
|
|
432
|
+
throw new Error(`页码无效`);
|
|
433
|
+
}
|
|
434
|
+
if (limit < 1 || limit > 1000) {
|
|
435
|
+
throw new Error(`每页数量无效`);
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
expect(mockValidate(1, 1)).toBe(true); // 最小值
|
|
441
|
+
expect(mockValidate(10000, 1000)).toBe(true); // 最大值
|
|
442
|
+
expect(mockValidate(5000, 500)).toBe(true); // 中间值
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe('DbHelper - 总数为 0 的优化', () => {
|
|
447
|
+
test('getList - 总数为 0 应直接返回空结果', async () => {
|
|
448
|
+
const mockGetList = async (total: number, page: number, limit: number) => {
|
|
449
|
+
if (total === 0) {
|
|
450
|
+
return {
|
|
451
|
+
lists: [],
|
|
452
|
+
total: 0,
|
|
453
|
+
page: page,
|
|
454
|
+
limit: limit,
|
|
455
|
+
pages: 0
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// 执行第二次查询...
|
|
459
|
+
return {
|
|
460
|
+
lists: [{ id: 1 }],
|
|
461
|
+
total: total,
|
|
462
|
+
page: page,
|
|
463
|
+
limit: limit,
|
|
464
|
+
pages: Math.ceil(total / limit)
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const result = await mockGetList(0, 1, 10);
|
|
469
|
+
expect(result.lists.length).toBe(0);
|
|
470
|
+
expect(result.total).toBe(0);
|
|
471
|
+
expect(result.pages).toBe(0);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('getList - pages 计算应正确', () => {
|
|
475
|
+
const calcPages = (total: number, limit: number) => Math.ceil(total / limit);
|
|
476
|
+
|
|
477
|
+
expect(calcPages(100, 10)).toBe(10); // 整除
|
|
478
|
+
expect(calcPages(105, 10)).toBe(11); // 有余数
|
|
479
|
+
expect(calcPages(5, 10)).toBe(1); // 小于一页
|
|
480
|
+
expect(calcPages(0, 10)).toBe(0); // 空结果
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('DbHelper - 字段清理逻辑', () => {
|
|
485
|
+
test('cleanFields - 应排除 null 和 undefined', () => {
|
|
486
|
+
const mockClean = (data: any) => {
|
|
487
|
+
const result: any = {};
|
|
488
|
+
for (const [key, value] of Object.entries(data)) {
|
|
489
|
+
if (value !== null && value !== undefined) {
|
|
490
|
+
result[key] = value;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const data = { name: 'test', age: null, email: undefined, score: 0 };
|
|
497
|
+
const result = mockClean(data);
|
|
498
|
+
|
|
499
|
+
expect(result.name).toBe('test');
|
|
500
|
+
expect(result.age).toBeUndefined();
|
|
501
|
+
expect(result.email).toBeUndefined();
|
|
502
|
+
expect(result.score).toBe(0); // 0 应保留
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('cleanFields - 空对象应返回空对象', () => {
|
|
506
|
+
const mockClean = (data: any) => {
|
|
507
|
+
if (!data || Object.keys(data).length === 0) return {};
|
|
508
|
+
return data;
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
expect(mockClean({})).toEqual({});
|
|
512
|
+
expect(mockClean(null)).toEqual({});
|
|
513
|
+
expect(mockClean(undefined)).toEqual({});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('DbHelper - SQL 执行错误处理', () => {
|
|
518
|
+
test('executeWithConn - SQL 错误应包含完整信息', () => {
|
|
519
|
+
const mockExecute = async (sql: string, params?: any[]) => {
|
|
520
|
+
try {
|
|
521
|
+
// 模拟 SQL 错误
|
|
522
|
+
throw new Error('Table not found');
|
|
523
|
+
} catch (error: any) {
|
|
524
|
+
const errorMsg = `SQL 执行失败 - ${error.message} - SQL: ${sql} - 参数: ${JSON.stringify(params)}`;
|
|
525
|
+
throw new Error(errorMsg);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
expect(async () => {
|
|
530
|
+
await mockExecute('SELECT * FROM non_existent_table', []);
|
|
531
|
+
}).toThrow();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('executeWithConn - 超长 SQL 应截断', () => {
|
|
535
|
+
const mockTruncate = (sql: string, maxLength: number = 500) => {
|
|
536
|
+
if (sql.length > maxLength) {
|
|
537
|
+
return sql.substring(0, maxLength) + '...';
|
|
538
|
+
}
|
|
539
|
+
return sql;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const longSql = 'SELECT * FROM users WHERE ' + 'name = ? AND '.repeat(100) + 'id = ?';
|
|
543
|
+
const truncated = mockTruncate(longSql, 100);
|
|
544
|
+
|
|
545
|
+
expect(truncated.length).toBeLessThanOrEqual(103); // 100 + "..."
|
|
546
|
+
expect(truncated.endsWith('...')).toBe(true);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe('DbHelper - 代码逻辑问题分析', () => {
|
|
551
|
+
test('问题1:addDefaultStateFilter 可能覆盖用户的 state 条件', () => {
|
|
552
|
+
// **问题描述**:
|
|
553
|
+
// 当前实现:addDefaultStateFilter 直接添加 state$gt: 0
|
|
554
|
+
// 问题:如果用户想查询 state=0(已删除)或 state=2(已禁用)的数据,
|
|
555
|
+
// 默认过滤会导致查询失败
|
|
556
|
+
|
|
557
|
+
// **建议修复**:
|
|
558
|
+
// 检查 where 条件中是否已有 state 相关字段
|
|
559
|
+
const currentImpl = (where: any) => {
|
|
560
|
+
return {
|
|
561
|
+
...where,
|
|
562
|
+
state$gt: 0 // 问题:直接覆盖
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const betterImpl = (where: any) => {
|
|
567
|
+
// 检查是否已有 state 条件
|
|
568
|
+
const hasStateCondition = Object.keys(where).some((key) => key === 'state' || key.startsWith('state$'));
|
|
569
|
+
|
|
570
|
+
if (hasStateCondition) {
|
|
571
|
+
return where; // 不添加默认过滤
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
...where,
|
|
576
|
+
state$gt: 0
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// 当前实现的问题
|
|
581
|
+
const result1 = currentImpl({ userId: 123, state: 0 });
|
|
582
|
+
expect(result1.state).toBe(0);
|
|
583
|
+
expect(result1.state$gt).toBe(0); // 冲突!
|
|
584
|
+
|
|
585
|
+
// 改进后的实现
|
|
586
|
+
const result2 = betterImpl({ userId: 123, state: 0 });
|
|
587
|
+
expect(result2.state).toBe(0);
|
|
588
|
+
expect(result2.state$gt).toBeUndefined(); // 正确
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('问题2:getTableColumns 缓存键没有区分数据库', () => {
|
|
592
|
+
// **问题描述**:
|
|
593
|
+
// 缓存键格式:table:columns:${table}
|
|
594
|
+
// 问题:如果连接多个数据库,不同数据库的同名表会共享缓存
|
|
595
|
+
|
|
596
|
+
// **建议修复**:
|
|
597
|
+
// 缓存键应包含数据库名:table:columns:${dbName}:${table}
|
|
598
|
+
|
|
599
|
+
const currentCacheKey = (table: string) => `table:columns:${table}`;
|
|
600
|
+
|
|
601
|
+
const betterCacheKey = (dbName: string, table: string) => `table:columns:${dbName}:${table}`;
|
|
602
|
+
|
|
603
|
+
// 问题示例
|
|
604
|
+
expect(currentCacheKey('user')).toBe('table:columns:user');
|
|
605
|
+
// db1.user 和 db2.user 会冲突
|
|
606
|
+
|
|
607
|
+
// 改进后
|
|
608
|
+
expect(betterCacheKey('db1', 'user')).toBe('table:columns:db1:user');
|
|
609
|
+
expect(betterCacheKey('db2', 'user')).toBe('table:columns:db2:user');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test('问题3:convertBigIntFields 白名单硬编码', () => {
|
|
613
|
+
// **问题描述**:
|
|
614
|
+
// 白名单字段硬编码为 ['id', 'pid', 'sort']
|
|
615
|
+
// 问题:如果有其他 BIGINT 字段不符合命名规则,需要修改源码
|
|
616
|
+
|
|
617
|
+
// **建议改进**:
|
|
618
|
+
// 1. 支持自定义白名单
|
|
619
|
+
// 2. 或者从表结构元数据中自动获取 BIGINT 字段
|
|
620
|
+
|
|
621
|
+
const currentImpl = (arr: any[]) => {
|
|
622
|
+
const whiteList = ['id', 'pid', 'sort']; // 硬编码
|
|
623
|
+
return arr.map((item) => {
|
|
624
|
+
const converted = { ...item };
|
|
625
|
+
for (const key of whiteList) {
|
|
626
|
+
if (key in converted && typeof converted[key] === 'string') {
|
|
627
|
+
converted[key] = Number(converted[key]);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return converted;
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const betterImpl = (arr: any[], customWhiteList?: string[]) => {
|
|
635
|
+
const defaultWhiteList = ['id', 'pid', 'sort'];
|
|
636
|
+
const whiteList = customWhiteList || defaultWhiteList;
|
|
637
|
+
|
|
638
|
+
return arr.map((item) => {
|
|
639
|
+
const converted = { ...item };
|
|
640
|
+
for (const key of whiteList) {
|
|
641
|
+
if (key in converted && typeof converted[key] === 'string') {
|
|
642
|
+
converted[key] = Number(converted[key]);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return converted;
|
|
646
|
+
});
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
// 改进后支持自定义
|
|
650
|
+
const data = [{ id: '123', customId: '456' }];
|
|
651
|
+
const result = betterImpl(data, ['id', 'customId']);
|
|
652
|
+
expect(result[0].customId).toBe(456);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('问题4:executeWithConn 没有超时保护', async () => {
|
|
656
|
+
// **问题描述**:
|
|
657
|
+
// 当前实现没有查询超时限制
|
|
658
|
+
// 问题:慢查询可能导致长时间阻塞
|
|
659
|
+
|
|
660
|
+
// **建议修复**:
|
|
661
|
+
// 添加查询超时机制
|
|
662
|
+
|
|
663
|
+
const mockExecuteWithTimeout = async (sql: string, params: any[], timeout: number = 30000) => {
|
|
664
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
665
|
+
setTimeout(() => reject(new Error('查询超时')), timeout);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const queryPromise = new Promise((resolve) => {
|
|
669
|
+
setTimeout(() => resolve([{ id: 1 }]), 100); // 模拟查询
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return Promise.race([queryPromise, timeoutPromise]);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// 正常查询
|
|
676
|
+
const result = await mockExecuteWithTimeout('SELECT * FROM users', [], 1000);
|
|
677
|
+
expect(result).toBeDefined();
|
|
678
|
+
|
|
679
|
+
// 超时查询
|
|
680
|
+
try {
|
|
681
|
+
await mockExecuteWithTimeout('SELECT * FROM users', [], 10); // 10ms 超时
|
|
682
|
+
expect(true).toBe(false); // 不应执行到这里
|
|
683
|
+
} catch (error: any) {
|
|
684
|
+
expect(error.message).toContain('查询超时');
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('问题5:getAll 的 MAX_LIMIT 保护不够完善', async () => {
|
|
689
|
+
// **问题描述**:
|
|
690
|
+
// getAll 设置了 MAX_LIMIT = 10000
|
|
691
|
+
// 问题:但没有检测实际查询的数据量,可能超过限制
|
|
692
|
+
|
|
693
|
+
// **建议改进**:
|
|
694
|
+
// 1. 查询前先检查总数
|
|
695
|
+
// 2. 如果超过限制,要求用户使用分页
|
|
696
|
+
|
|
697
|
+
const betterGetAll = async (table: string, MAX_LIMIT: number = 10000) => {
|
|
698
|
+
// 先查询总数
|
|
699
|
+
const total = 15000; // 模拟
|
|
700
|
+
|
|
701
|
+
if (total > MAX_LIMIT) {
|
|
702
|
+
throw new Error(`数据量过大 (${total} 条),请使用 getList 分页查询`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 执行查询...
|
|
706
|
+
return [];
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
await betterGetAll('users', 10000);
|
|
711
|
+
expect(true).toBe(false);
|
|
712
|
+
} catch (error: any) {
|
|
713
|
+
expect(error.message).toContain('数据量过大');
|
|
714
|
+
expect(error.message).toContain('请使用 getList 分页查询');
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|