befly 3.10.0 → 3.10.2
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/.gitignore +0 -0
- package/README.md +10 -13
- package/configs/presetFields.ts +10 -0
- package/configs/presetRegexp.ts +225 -0
- package/docs/README.md +17 -11
- package/docs/api/api.md +15 -1
- package/docs/guide/quickstart.md +19 -5
- package/docs/infra/redis.md +23 -11
- package/docs/quickstart.md +5 -335
- package/docs/reference/addon.md +0 -15
- package/docs/reference/config.md +1 -1
- package/docs/reference/logger.md +3 -3
- package/docs/reference/sync.md +99 -73
- package/docs/reference/table.md +1 -1
- package/package.json +15 -16
- package/docs/cipher.md +0 -582
- package/docs/database.md +0 -1176
- package/tests/_mocks/mockSqliteDb.ts +0 -204
- package/tests/addonHelper-cache.test.ts +0 -32
- package/tests/api-integration-array-number.test.ts +0 -282
- package/tests/apiHandler-routePath-only.test.ts +0 -32
- package/tests/befly-config-env.test.ts +0 -78
- package/tests/cacheHelper.test.ts +0 -323
- package/tests/cacheKeys.test.ts +0 -41
- package/tests/checkApi-routePath-strict.test.ts +0 -166
- package/tests/checkMenu.test.ts +0 -346
- package/tests/checkTable-smoke.test.ts +0 -157
- package/tests/cipher.test.ts +0 -249
- package/tests/dbDialect-cache.test.ts +0 -23
- package/tests/dbDialect.test.ts +0 -46
- package/tests/dbHelper-advanced.test.ts +0 -723
- package/tests/dbHelper-all-array-types.test.ts +0 -316
- package/tests/dbHelper-array-serialization.test.ts +0 -258
- package/tests/dbHelper-batch-write.test.ts +0 -90
- package/tests/dbHelper-columns.test.ts +0 -234
- package/tests/dbHelper-execute.test.ts +0 -187
- package/tests/dbHelper-joins.test.ts +0 -221
- package/tests/fields-redis-cache.test.ts +0 -127
- package/tests/fields-validate.test.ts +0 -99
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
- package/tests/fixtures/scanFilesApis/a.ts +0 -3
- package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
- package/tests/getClientIp.test.ts +0 -54
- package/tests/integration.test.ts +0 -189
- package/tests/jwt.test.ts +0 -65
- package/tests/loadPlugins-order-smoke.test.ts +0 -75
- package/tests/logger.test.ts +0 -325
- package/tests/redisHelper.test.ts +0 -495
- package/tests/redisKeys.test.ts +0 -9
- package/tests/scanConfig.test.ts +0 -144
- package/tests/scanFiles-routePath.test.ts +0 -46
- package/tests/smoke-sql.test.ts +0 -24
- package/tests/sqlBuilder-advanced.test.ts +0 -608
- package/tests/sqlBuilder.test.ts +0 -209
- package/tests/sync-connection.test.ts +0 -183
- package/tests/sync-init-guard.test.ts +0 -105
- package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
- package/tests/syncApi-obsolete-records.test.ts +0 -69
- package/tests/syncApi-type-compat.test.ts +0 -72
- package/tests/syncDev-permissions.test.ts +0 -81
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
- package/tests/syncMenu-duplicate-path.test.ts +0 -122
- package/tests/syncMenu-obsolete-records.test.ts +0 -161
- package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
- package/tests/syncMenu-paths.test.ts +0 -59
- package/tests/syncTable-apply.test.ts +0 -279
- package/tests/syncTable-array-number.test.ts +0 -160
- package/tests/syncTable-constants.test.ts +0 -101
- package/tests/syncTable-db-integration.test.ts +0 -237
- package/tests/syncTable-ddl.test.ts +0 -245
- package/tests/syncTable-helpers.test.ts +0 -99
- package/tests/syncTable-schema.test.ts +0 -99
- package/tests/syncTable-testkit.test.ts +0 -25
- package/tests/syncTable-types.test.ts +0 -122
- package/tests/tableRef-and-deserialize.test.ts +0 -67
- package/tests/util.test.ts +0 -100
- package/tests/validator-array-number.test.ts +0 -310
- package/tests/validator-default.test.ts +0 -373
- package/tests/validator.test.ts +0 -679
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 验证 Redis 缓存的字段查询功能
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
6
|
-
|
|
7
|
-
const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
|
|
8
|
-
|
|
9
|
-
console.log("\n========== Redis 缓存验证 ==========\n");
|
|
10
|
-
|
|
11
|
-
// 模拟 Redis 缓存逻辑
|
|
12
|
-
class MockRedis {
|
|
13
|
-
private cache: Map<string, { value: any; expire: number }> = new Map();
|
|
14
|
-
|
|
15
|
-
async getObject<T>(key: string): Promise<T | null> {
|
|
16
|
-
const cached = this.cache.get(key);
|
|
17
|
-
if (cached && cached.expire > Date.now()) {
|
|
18
|
-
console.log(`✅ Redis 缓存命中: ${key}`);
|
|
19
|
-
return cached.value as T;
|
|
20
|
-
}
|
|
21
|
-
console.log(`❌ Redis 缓存未命中: ${key}`);
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async setObject(key: string, value: any, ttl: number): Promise<void> {
|
|
26
|
-
this.cache.set(key, {
|
|
27
|
-
value: value,
|
|
28
|
-
expire: Date.now() + ttl * 1000
|
|
29
|
-
});
|
|
30
|
-
console.log(`📝 写入 Redis 缓存: ${key} (TTL: ${ttl}s)`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 模拟数据库查询
|
|
35
|
-
async function queryDatabase(table: string): Promise<string[]> {
|
|
36
|
-
console.log(`🔍 查询数据库表结构: ${table}`);
|
|
37
|
-
// 模拟数据库延迟
|
|
38
|
-
await new Promise((resolve) => setTimeout(resolve, 3));
|
|
39
|
-
return ["id", "name", "email", "password", "salt", "created_at"];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 模拟 getTableColumns 方法
|
|
43
|
-
async function getTableColumns(redis: MockRedis, table: string): Promise<string[]> {
|
|
44
|
-
// 1. 先查 Redis 缓存
|
|
45
|
-
const cacheKey = CacheKeys.tableColumns(table);
|
|
46
|
-
let columns = await redis.getObject<string[]>(cacheKey);
|
|
47
|
-
|
|
48
|
-
if (columns && columns.length > 0) {
|
|
49
|
-
return columns;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 2. 缓存未命中,查询数据库
|
|
53
|
-
columns = await queryDatabase(table);
|
|
54
|
-
|
|
55
|
-
// 3. 写入 Redis 缓存
|
|
56
|
-
await redis.setObject(cacheKey, columns, TABLE_COLUMNS_CACHE_TTL_SECONDS);
|
|
57
|
-
|
|
58
|
-
return columns;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function test() {
|
|
62
|
-
const redis = new MockRedis();
|
|
63
|
-
|
|
64
|
-
console.log("【场景1】单进程多次查询\n");
|
|
65
|
-
|
|
66
|
-
// 第1次查询(缓存未命中)
|
|
67
|
-
console.log("--- 第1次查询 user 表 ---");
|
|
68
|
-
const start1 = Date.now();
|
|
69
|
-
const columns1 = await getTableColumns(redis, "user");
|
|
70
|
-
const time1 = Date.now() - start1;
|
|
71
|
-
console.log(`结果: ${columns1.join(", ")}`);
|
|
72
|
-
console.log(`耗时: ${time1}ms\n`);
|
|
73
|
-
|
|
74
|
-
// 第2次查询(缓存命中)
|
|
75
|
-
console.log("--- 第2次查询 user 表 ---");
|
|
76
|
-
const start2 = Date.now();
|
|
77
|
-
const columns2 = await getTableColumns(redis, "user");
|
|
78
|
-
const time2 = Date.now() - start2;
|
|
79
|
-
console.log(`结果: ${columns2.join(", ")}`);
|
|
80
|
-
console.log(`耗时: ${time2}ms\n`);
|
|
81
|
-
|
|
82
|
-
// 第3次查询(缓存命中)
|
|
83
|
-
console.log("--- 第3次查询 user 表 ---");
|
|
84
|
-
const start3 = Date.now();
|
|
85
|
-
const columns3 = await getTableColumns(redis, "user");
|
|
86
|
-
const time3 = Date.now() - start3;
|
|
87
|
-
console.log(`结果: ${columns3.join(", ")}`);
|
|
88
|
-
console.log(`耗时: ${time3}ms\n`);
|
|
89
|
-
|
|
90
|
-
console.log("【场景2】模拟 PM2 cluster(多进程共享 Redis)\n");
|
|
91
|
-
|
|
92
|
-
// 模拟 Worker 1 查询
|
|
93
|
-
console.log("--- Worker 1 查询 article 表 ---");
|
|
94
|
-
const worker1Start = Date.now();
|
|
95
|
-
const worker1Columns = await getTableColumns(redis, "article");
|
|
96
|
-
const worker1Time = Date.now() - worker1Start;
|
|
97
|
-
console.log(`结果: ${worker1Columns.join(", ")}`);
|
|
98
|
-
console.log(`耗时: ${worker1Time}ms\n`);
|
|
99
|
-
|
|
100
|
-
// 模拟 Worker 2 查询(共享 Redis 缓存)
|
|
101
|
-
console.log("--- Worker 2 查询 article 表 ---");
|
|
102
|
-
const worker2Start = Date.now();
|
|
103
|
-
const worker2Columns = await getTableColumns(redis, "article");
|
|
104
|
-
const worker2Time = Date.now() - worker2Start;
|
|
105
|
-
console.log(`结果: ${worker2Columns.join(", ")}`);
|
|
106
|
-
console.log(`耗时: ${worker2Time}ms`);
|
|
107
|
-
console.log(`✅ Worker 2 直接使用 Worker 1 的缓存,无需再查数据库\n`);
|
|
108
|
-
|
|
109
|
-
// 模拟 Worker 3 查询(共享 Redis 缓存)
|
|
110
|
-
console.log("--- Worker 3 查询 article 表 ---");
|
|
111
|
-
const worker3Start = Date.now();
|
|
112
|
-
const worker3Columns = await getTableColumns(redis, "article");
|
|
113
|
-
const worker3Time = Date.now() - worker3Start;
|
|
114
|
-
console.log(`结果: ${worker3Columns.join(", ")}`);
|
|
115
|
-
console.log(`耗时: ${worker3Time}ms`);
|
|
116
|
-
console.log(`✅ Worker 3 直接使用 Worker 1 的缓存,无需再查数据库\n`);
|
|
117
|
-
|
|
118
|
-
console.log("========== 验证完成 ==========\n");
|
|
119
|
-
|
|
120
|
-
console.log("📊 性能总结:");
|
|
121
|
-
console.log(`- 首次查询(数据库): ${time1}ms`);
|
|
122
|
-
console.log(`- 后续查询(Redis): ${time2}ms`);
|
|
123
|
-
console.log(`- 性能提升: ${(time1 / time2).toFixed(1)}x`);
|
|
124
|
-
console.log(`- PM2 cluster: ✅ 所有 worker 共享同一份 Redis 缓存`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
test();
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 简单测试字段排除功能
|
|
3
|
-
* 直接测试 validateAndClassifyFields 方法
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// 模拟测试函数(从 dbHelper 复制的逻辑)
|
|
7
|
-
function validateAndClassifyFields(fields?: string[]): {
|
|
8
|
-
type: "all" | "include" | "exclude";
|
|
9
|
-
fields: string[];
|
|
10
|
-
} {
|
|
11
|
-
// 情况1:空数组或 undefined,表示查询所有
|
|
12
|
-
if (!fields || fields.length === 0) {
|
|
13
|
-
return { type: "all", fields: [] };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// 检测是否有星号(禁止)
|
|
17
|
-
if (fields.some((f) => f === "*")) {
|
|
18
|
-
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// 检测是否有空字符串或无效值
|
|
22
|
-
if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
|
|
23
|
-
throw new Error("fields 不能包含空字符串或无效值");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 统计包含字段和排除字段
|
|
27
|
-
const includeFields = fields.filter((f) => !f.startsWith("!"));
|
|
28
|
-
const excludeFields = fields.filter((f) => f.startsWith("!"));
|
|
29
|
-
|
|
30
|
-
// 情况2:全部是包含字段
|
|
31
|
-
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
32
|
-
return { type: "include", fields: includeFields };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// 情况3:全部是排除字段
|
|
36
|
-
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
37
|
-
// 去掉感叹号前缀
|
|
38
|
-
const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
|
|
39
|
-
return { type: "exclude", fields: cleanExcludeFields };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 混用情况:报错
|
|
43
|
-
throw new Error('fields 不能同时包含普通字段和排除字段(! 开头)。只能使用以下3种方式之一:\n1. 空数组 [] 或不传(查询所有)\n2. 全部指定字段 ["id", "name"]\n3. 全部排除字段 ["!password", "!token"]');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.log("\n========== 测试 validateAndClassifyFields ==========\n");
|
|
47
|
-
|
|
48
|
-
// 测试1:空数组
|
|
49
|
-
console.log("【测试1】空数组");
|
|
50
|
-
const test1 = validateAndClassifyFields([]);
|
|
51
|
-
console.log("结果:", test1);
|
|
52
|
-
console.log("✅ 通过\n");
|
|
53
|
-
|
|
54
|
-
// 测试2:undefined
|
|
55
|
-
console.log("【测试2】undefined");
|
|
56
|
-
const test2 = validateAndClassifyFields(undefined);
|
|
57
|
-
console.log("结果:", test2);
|
|
58
|
-
console.log("✅ 通过\n");
|
|
59
|
-
|
|
60
|
-
// 测试3:包含字段
|
|
61
|
-
console.log("【测试3】包含字段");
|
|
62
|
-
const test3 = validateAndClassifyFields(["id", "name", "email"]);
|
|
63
|
-
console.log("结果:", test3);
|
|
64
|
-
console.log("✅ 通过\n");
|
|
65
|
-
|
|
66
|
-
// 测试4:排除字段
|
|
67
|
-
console.log("【测试4】排除字段");
|
|
68
|
-
const test4 = validateAndClassifyFields(["!password", "!salt"]);
|
|
69
|
-
console.log("结果:", test4);
|
|
70
|
-
console.log("✅ 通过\n");
|
|
71
|
-
|
|
72
|
-
// 测试5:混用(应该报错)
|
|
73
|
-
console.log("【测试5】混用(应该报错)");
|
|
74
|
-
try {
|
|
75
|
-
validateAndClassifyFields(["id", "!password"]);
|
|
76
|
-
console.log("❌ 没有抛出错误\n");
|
|
77
|
-
} catch (error: any) {
|
|
78
|
-
console.log("✅ 成功捕获错误:", error.message, "\n");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 测试6:星号(应该报错)
|
|
82
|
-
console.log("【测试6】星号(应该报错)");
|
|
83
|
-
try {
|
|
84
|
-
validateAndClassifyFields(["*"]);
|
|
85
|
-
console.log("❌ 没有抛出错误\n");
|
|
86
|
-
} catch (error: any) {
|
|
87
|
-
console.log("✅ 成功捕获错误:", error.message, "\n");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 测试7:空字符串(应该报错)
|
|
91
|
-
console.log("【测试7】空字符串(应该报错)");
|
|
92
|
-
try {
|
|
93
|
-
validateAndClassifyFields(["id", "", "name"]);
|
|
94
|
-
console.log("❌ 没有抛出错误\n");
|
|
95
|
-
} catch (error: any) {
|
|
96
|
-
console.log("✅ 成功捕获错误:", error.message, "\n");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log("========== 所有测试完成 ==========\n");
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { getClientIp } from "../utils/getClientIp";
|
|
4
|
-
|
|
5
|
-
describe("utils - getClientIp", () => {
|
|
6
|
-
test("优先使用 x-forwarded-for 的第一个值", () => {
|
|
7
|
-
const req = new Request("http://localhost/api/test", {
|
|
8
|
-
method: "GET",
|
|
9
|
-
headers: {
|
|
10
|
-
"x-forwarded-for": " 8.8.8.8, 1.1.1.1 "
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const ip = getClientIp(req);
|
|
15
|
-
expect(ip).toBe("8.8.8.8");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("x-forwarded-for 为空时回退到 x-real-ip", () => {
|
|
19
|
-
const req = new Request("http://localhost/api/test", {
|
|
20
|
-
method: "GET",
|
|
21
|
-
headers: {
|
|
22
|
-
"x-forwarded-for": " ",
|
|
23
|
-
"x-real-ip": "9.9.9.9"
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const ip = getClientIp(req);
|
|
28
|
-
expect(ip).toBe("9.9.9.9");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("无代理头时使用 server.requestIP(req) 兜底", () => {
|
|
32
|
-
const req = new Request("http://localhost/api/test", {
|
|
33
|
-
method: "GET"
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const server = {
|
|
37
|
-
requestIP(_req: Request) {
|
|
38
|
-
return { address: "7.7.7.7" };
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const ip = getClientIp(req, server);
|
|
43
|
-
expect(ip).toBe("7.7.7.7");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("全都没有时返回 unknown", () => {
|
|
47
|
-
const req = new Request("http://localhost/api/test", {
|
|
48
|
-
method: "GET"
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const ip = getClientIp(req);
|
|
52
|
-
expect(ip).toBe("unknown");
|
|
53
|
-
});
|
|
54
|
-
});
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { XMLParser } from "fast-xml-parser";
|
|
4
|
-
|
|
5
|
-
import { Cipher } from "../lib/cipher";
|
|
6
|
-
import { Jwt } from "../lib/jwt";
|
|
7
|
-
import { SqlBuilder } from "../lib/sqlBuilder";
|
|
8
|
-
import { Validator } from "../lib/validator";
|
|
9
|
-
import { keysToCamel } from "../utils/keysToCamel";
|
|
10
|
-
import { keysToSnake } from "../utils/keysToSnake";
|
|
11
|
-
|
|
12
|
-
describe("Integration - 密码验证流程", () => {
|
|
13
|
-
test("用户注册:密码加密 + 验证", async () => {
|
|
14
|
-
const password = "MySecurePass123";
|
|
15
|
-
|
|
16
|
-
// 1. 密码加密
|
|
17
|
-
const hashedPassword = await Cipher.hashPassword(password);
|
|
18
|
-
expect(hashedPassword).toBeDefined();
|
|
19
|
-
expect(hashedPassword.length).toBeGreaterThan(0);
|
|
20
|
-
|
|
21
|
-
// 2. 密码验证
|
|
22
|
-
const isValid = await Cipher.verifyPassword(password, hashedPassword);
|
|
23
|
-
expect(isValid).toBe(true);
|
|
24
|
-
|
|
25
|
-
// 3. 错误密码验证
|
|
26
|
-
const isInvalid = await Cipher.verifyPassword("WrongPassword", hashedPassword);
|
|
27
|
-
expect(isInvalid).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("Integration - JWT + 权限验证", () => {
|
|
32
|
-
const jwt = new Jwt({
|
|
33
|
-
secret: "test-integration-secret",
|
|
34
|
-
algorithm: "HS256",
|
|
35
|
-
expiresIn: "1h"
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("用户登录:JWT 签名 + 验证", () => {
|
|
39
|
-
// 1. 用户登录生成 token
|
|
40
|
-
const payload = {
|
|
41
|
-
userId: 123,
|
|
42
|
-
username: "john",
|
|
43
|
-
roles: ["admin", "user"],
|
|
44
|
-
permissions: ["read", "write", "delete"]
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const token = jwt.sign(payload);
|
|
48
|
-
expect(token).toBeDefined();
|
|
49
|
-
expect(typeof token).toBe("string");
|
|
50
|
-
|
|
51
|
-
// 2. 验证 token
|
|
52
|
-
const verified = jwt.verify(token);
|
|
53
|
-
expect(verified.userId).toBe(123);
|
|
54
|
-
expect(verified.username).toBe("john");
|
|
55
|
-
expect(verified.roles).toContain("admin");
|
|
56
|
-
expect(verified.permissions).toContain("write");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("Integration - 数据验证 + SQL 构建", () => {
|
|
61
|
-
test("API 请求:验证数据 + 构建查询", () => {
|
|
62
|
-
// 1. 验证用户输入
|
|
63
|
-
const userData = {
|
|
64
|
-
email: "test@example.com",
|
|
65
|
-
age: 25,
|
|
66
|
-
username: "john"
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const rules = {
|
|
70
|
-
email: { name: "邮箱", type: "string", regexp: "@email" },
|
|
71
|
-
age: { name: "年龄", type: "number", min: 0, max: 150 },
|
|
72
|
-
username: { name: "用户名", type: "string", min: 2, max: 20 }
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const validationResult = Validator.validate(userData, rules, ["email", "username"]);
|
|
76
|
-
expect(validationResult.code).toBe(0);
|
|
77
|
-
|
|
78
|
-
// 2. 验证通过后构建 SQL 查询
|
|
79
|
-
const builder = new SqlBuilder();
|
|
80
|
-
const sqlResult = builder.select(["id", "username", "email"]).from("users").where({ email: userData.email }).toSelectSql();
|
|
81
|
-
|
|
82
|
-
expect(sqlResult.sql).toContain("SELECT");
|
|
83
|
-
expect(sqlResult.sql).toContain("FROM `users`");
|
|
84
|
-
expect(sqlResult.sql).toContain("WHERE `email` = ?");
|
|
85
|
-
expect(sqlResult.params).toContain("test@example.com");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("数据插入:验证 + 字段转换 + SQL 构建", () => {
|
|
89
|
-
// 1. 验证数据
|
|
90
|
-
const newUser = {
|
|
91
|
-
userName: "jane",
|
|
92
|
-
userEmail: "jane@example.com",
|
|
93
|
-
userAge: 30
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const rules = {
|
|
97
|
-
userName: { name: "用户名", type: "string", min: 2, max: 20 },
|
|
98
|
-
userEmail: { name: "邮箱", type: "string", regexp: "@email" },
|
|
99
|
-
userAge: { name: "年龄", type: "number", min: 0, max: 150 }
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const validationResult = Validator.validate(newUser, rules, ["userName", "userEmail"]);
|
|
103
|
-
expect(validationResult.code).toBe(0);
|
|
104
|
-
|
|
105
|
-
// 2. 字段转换(驼峰转下划线)
|
|
106
|
-
const dbData = keysToSnake(newUser);
|
|
107
|
-
expect(dbData.user_name).toBe("jane");
|
|
108
|
-
expect(dbData.user_email).toBe("jane@example.com");
|
|
109
|
-
expect(dbData.user_age).toBe(30);
|
|
110
|
-
|
|
111
|
-
// 3. 构建插入 SQL
|
|
112
|
-
const builder = new SqlBuilder();
|
|
113
|
-
const sqlResult = builder.toInsertSql("users", dbData);
|
|
114
|
-
|
|
115
|
-
expect(sqlResult.sql).toContain("INSERT INTO `users`");
|
|
116
|
-
expect(sqlResult.params).toContain("jane");
|
|
117
|
-
expect(sqlResult.params).toContain("jane@example.com");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe("Integration - XML 解析 + 数据转换", () => {
|
|
122
|
-
test("XML API 响应:解析 + 字段转换", () => {
|
|
123
|
-
const xmlParser = new XMLParser();
|
|
124
|
-
|
|
125
|
-
// 1. 解析 XML 响应
|
|
126
|
-
const xmlData = "<response><user_id>123</user_id><user_name>John</user_name><is_active>true</is_active></response>";
|
|
127
|
-
const parsed = xmlParser.parse(xmlData).response as any;
|
|
128
|
-
|
|
129
|
-
expect(parsed.user_id).toBe(123);
|
|
130
|
-
expect(parsed.user_name).toBe("John");
|
|
131
|
-
expect(parsed.is_active).toBe(true);
|
|
132
|
-
|
|
133
|
-
// 2. 字段转换(下划线转驼峰)
|
|
134
|
-
const camelData = keysToCamel(parsed);
|
|
135
|
-
expect(camelData.userId).toBe(123);
|
|
136
|
-
expect(camelData.userName).toBe("John");
|
|
137
|
-
expect(camelData.isActive).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe("Integration - 加密 + JWT + 哈希", () => {
|
|
142
|
-
test("安全令牌生成:多层加密", () => {
|
|
143
|
-
// 1. 生成随机字符串作为 secret
|
|
144
|
-
const secret = Cipher.randomString(32);
|
|
145
|
-
expect(secret.length).toBe(32);
|
|
146
|
-
|
|
147
|
-
// 2. 对 secret 进行 SHA256 哈希
|
|
148
|
-
const hashedSecret = Cipher.sha256(secret);
|
|
149
|
-
expect(hashedSecret.length).toBe(64);
|
|
150
|
-
|
|
151
|
-
// 3. 使用 HMAC 签名
|
|
152
|
-
const data = "user123:session456";
|
|
153
|
-
const signature = Cipher.hmacSha256(data, hashedSecret);
|
|
154
|
-
expect(signature).toBeDefined();
|
|
155
|
-
|
|
156
|
-
// 4. Base64 编码
|
|
157
|
-
const encoded = Cipher.base64Encode(signature);
|
|
158
|
-
expect(encoded).toBeDefined();
|
|
159
|
-
|
|
160
|
-
// 5. Base64 解码验证
|
|
161
|
-
const decoded = Cipher.base64Decode(encoded);
|
|
162
|
-
expect(decoded).toBe(signature);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("Integration - SQL 构建 + 字段转换", () => {
|
|
167
|
-
test("复杂查询:驼峰转下划线 + WHERE 条件 + 排序分页", () => {
|
|
168
|
-
const builder = new SqlBuilder();
|
|
169
|
-
|
|
170
|
-
// 1. 原始查询条件(驼峰格式)
|
|
171
|
-
const queryConditions = {
|
|
172
|
-
userId: 123,
|
|
173
|
-
userStatus: "active",
|
|
174
|
-
createdAt: { $gte: 1609459200000 }
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// 2. 转换为下划线格式
|
|
178
|
-
const dbConditions = keysToSnake(queryConditions);
|
|
179
|
-
|
|
180
|
-
// 3. 构建复杂查询
|
|
181
|
-
const sqlResult = builder.select(["id", "user_name", "user_email", "created_at"]).from("users").where(dbConditions).orderBy(["created_at#DESC"]).limit(20).offset(0).toSelectSql();
|
|
182
|
-
|
|
183
|
-
expect(sqlResult.sql).toContain("SELECT");
|
|
184
|
-
expect(sqlResult.sql).toContain("WHERE");
|
|
185
|
-
expect(sqlResult.sql).toContain("ORDER BY");
|
|
186
|
-
expect(sqlResult.sql).toContain("LIMIT 20");
|
|
187
|
-
expect(sqlResult.params.length).toBeGreaterThan(0);
|
|
188
|
-
});
|
|
189
|
-
});
|
package/tests/jwt.test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { Jwt } from "../lib/jwt";
|
|
4
|
-
|
|
5
|
-
const jwt = new Jwt({
|
|
6
|
-
secret: "test-secret",
|
|
7
|
-
algorithm: "HS256",
|
|
8
|
-
expiresIn: "7d"
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe("JWT - sign", () => {
|
|
12
|
-
test("签名创建 token", () => {
|
|
13
|
-
const token = jwt.sign({ userId: 1, name: "test" });
|
|
14
|
-
expect(typeof token).toBe("string");
|
|
15
|
-
expect(token.split(".").length).toBe(3);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("自定义密钥", () => {
|
|
19
|
-
const secret = "custom-secret";
|
|
20
|
-
const token = jwt.sign({ userId: 1 }, { secret: secret });
|
|
21
|
-
const decoded = jwt.verify(token, { secret: secret });
|
|
22
|
-
expect(decoded.userId).toBe(1);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("JWT - verify", () => {
|
|
27
|
-
test("验证有效 token", () => {
|
|
28
|
-
const token = jwt.sign({ userId: 123, email: "test@test.com" });
|
|
29
|
-
const decoded = jwt.verify(token);
|
|
30
|
-
expect(decoded.userId).toBe(123);
|
|
31
|
-
expect(decoded.email).toBe("test@test.com");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("验证过期 token 抛出错误", () => {
|
|
35
|
-
const token = jwt.sign({ userId: 1 }, { expiresIn: "-1s" });
|
|
36
|
-
expect(() => jwt.verify(token)).toThrow();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("忽略过期验证", () => {
|
|
40
|
-
const token = jwt.sign({ userId: 1 }, { expiresIn: "-1s" });
|
|
41
|
-
const decoded = jwt.verify(token, { ignoreExpiration: true });
|
|
42
|
-
expect(decoded.userId).toBe(1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("密钥不匹配验证失败", () => {
|
|
46
|
-
const token = jwt.sign({ userId: 1 }, { secret: "secret1" });
|
|
47
|
-
expect(() => jwt.verify(token, { secret: "secret2" })).toThrow();
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("JWT - decode", () => {
|
|
52
|
-
test("解码不验证签名", () => {
|
|
53
|
-
const token = jwt.sign({ userId: 456 });
|
|
54
|
-
const decoded = jwt.decode(token);
|
|
55
|
-
expect(decoded.userId).toBe(456);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("解码完整信息", () => {
|
|
59
|
-
const token = jwt.sign({ userId: 1 });
|
|
60
|
-
const decoded = jwt.decode(token, true);
|
|
61
|
-
expect(decoded.header).toBeDefined();
|
|
62
|
-
expect(decoded.payload).toBeDefined();
|
|
63
|
-
expect(decoded.signature).toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { loadPlugins } from "../loader/loadPlugins.js";
|
|
4
|
-
|
|
5
|
-
describe("loadPlugins - order smoke", () => {
|
|
6
|
-
test("deps 应确保 redis 在 db/cache 之前初始化", async () => {
|
|
7
|
-
const executed: string[] = [];
|
|
8
|
-
const ctx: any = {};
|
|
9
|
-
|
|
10
|
-
const plugins: any[] = [
|
|
11
|
-
{
|
|
12
|
-
moduleName: "db",
|
|
13
|
-
deps: ["logger", "redis"],
|
|
14
|
-
handler: async (befly: any) => {
|
|
15
|
-
executed.push("db");
|
|
16
|
-
|
|
17
|
-
if (!befly.redis) {
|
|
18
|
-
throw new Error("db handler called before redis");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return { ok: true };
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
moduleName: "cache",
|
|
26
|
-
deps: ["logger", "redis", "db"],
|
|
27
|
-
handler: async (befly: any) => {
|
|
28
|
-
executed.push("cache");
|
|
29
|
-
|
|
30
|
-
if (!befly.redis) {
|
|
31
|
-
throw new Error("cache handler called before redis");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!befly.db) {
|
|
35
|
-
throw new Error("cache handler called before db");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { ok: true };
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
moduleName: "redis",
|
|
43
|
-
deps: ["logger"],
|
|
44
|
-
handler: async (_befly: any) => {
|
|
45
|
-
executed.push("redis");
|
|
46
|
-
return { ok: true };
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
moduleName: "logger",
|
|
51
|
-
deps: [],
|
|
52
|
-
handler: async (_befly: any) => {
|
|
53
|
-
executed.push("logger");
|
|
54
|
-
return { ok: true };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
await loadPlugins(plugins as any, ctx as any, []);
|
|
60
|
-
|
|
61
|
-
const loggerIndex = executed.indexOf("logger");
|
|
62
|
-
const redisIndex = executed.indexOf("redis");
|
|
63
|
-
const dbIndex = executed.indexOf("db");
|
|
64
|
-
const cacheIndex = executed.indexOf("cache");
|
|
65
|
-
|
|
66
|
-
expect(loggerIndex).toBeGreaterThanOrEqual(0);
|
|
67
|
-
expect(redisIndex).toBeGreaterThanOrEqual(0);
|
|
68
|
-
expect(dbIndex).toBeGreaterThanOrEqual(0);
|
|
69
|
-
expect(cacheIndex).toBeGreaterThanOrEqual(0);
|
|
70
|
-
|
|
71
|
-
expect(loggerIndex).toBeLessThan(redisIndex);
|
|
72
|
-
expect(redisIndex).toBeLessThan(dbIndex);
|
|
73
|
-
expect(dbIndex).toBeLessThan(cacheIndex);
|
|
74
|
-
});
|
|
75
|
-
});
|