befly 3.8.19 → 3.8.21

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 (78) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +1 -1
  3. package/checks/checkApi.ts +92 -0
  4. package/checks/checkApp.ts +31 -0
  5. package/{check.ts → checks/checkTable.ts} +28 -159
  6. package/config.ts +71 -0
  7. package/hooks/auth.ts +30 -0
  8. package/hooks/cors.ts +48 -0
  9. package/hooks/errorHandler.ts +23 -0
  10. package/hooks/parser.ts +67 -0
  11. package/hooks/permission.ts +54 -0
  12. package/hooks/rateLimit.ts +70 -0
  13. package/hooks/requestId.ts +24 -0
  14. package/hooks/requestLogger.ts +25 -0
  15. package/hooks/responseFormatter.ts +64 -0
  16. package/hooks/validator.ts +34 -0
  17. package/lib/database.ts +28 -25
  18. package/lib/dbHelper.ts +3 -3
  19. package/lib/jwt.ts +90 -99
  20. package/lib/logger.ts +44 -23
  21. package/lib/redisHelper.ts +19 -22
  22. package/loader/loadApis.ts +69 -114
  23. package/loader/loadHooks.ts +65 -0
  24. package/loader/loadPlugins.ts +50 -219
  25. package/main.ts +106 -133
  26. package/package.json +23 -14
  27. package/paths.ts +20 -0
  28. package/plugins/cache.ts +1 -3
  29. package/plugins/db.ts +8 -11
  30. package/plugins/logger.ts +5 -3
  31. package/plugins/redis.ts +10 -14
  32. package/router/api.ts +60 -106
  33. package/router/root.ts +15 -12
  34. package/router/static.ts +54 -58
  35. package/sync/syncAll.ts +58 -0
  36. package/sync/syncApi.ts +264 -0
  37. package/sync/syncDb/apply.ts +194 -0
  38. package/sync/syncDb/constants.ts +76 -0
  39. package/sync/syncDb/ddl.ts +194 -0
  40. package/sync/syncDb/helpers.ts +200 -0
  41. package/sync/syncDb/index.ts +164 -0
  42. package/sync/syncDb/schema.ts +201 -0
  43. package/sync/syncDb/sqlite.ts +50 -0
  44. package/sync/syncDb/table.ts +321 -0
  45. package/sync/syncDb/tableCreate.ts +146 -0
  46. package/sync/syncDb/version.ts +72 -0
  47. package/sync/syncDb.ts +19 -0
  48. package/sync/syncDev.ts +206 -0
  49. package/sync/syncMenu.ts +331 -0
  50. package/tests/cipher.test.ts +248 -0
  51. package/tests/dbHelper-advanced.test.ts +717 -0
  52. package/tests/dbHelper-columns.test.ts +266 -0
  53. package/tests/dbHelper-execute.test.ts +240 -0
  54. package/tests/fields-redis-cache.test.ts +123 -0
  55. package/tests/fields-validate.test.ts +99 -0
  56. package/tests/integration.test.ts +202 -0
  57. package/tests/jwt.test.ts +122 -0
  58. package/tests/logger.test.ts +94 -0
  59. package/tests/redisHelper.test.ts +231 -0
  60. package/tests/sqlBuilder-advanced.test.ts +593 -0
  61. package/tests/sqlBuilder.test.ts +184 -0
  62. package/tests/util.test.ts +95 -0
  63. package/tests/validator-advanced.test.ts +653 -0
  64. package/tests/validator.test.ts +148 -0
  65. package/tests/xml.test.ts +101 -0
  66. package/tsconfig.json +2 -4
  67. package/types/api.d.ts +6 -0
  68. package/types/befly.d.ts +152 -28
  69. package/types/context.d.ts +29 -3
  70. package/types/hook.d.ts +35 -0
  71. package/types/index.ts +14 -1
  72. package/types/plugin.d.ts +6 -7
  73. package/types/sync.d.ts +403 -0
  74. package/env.ts +0 -106
  75. package/lib/middleware.ts +0 -275
  76. package/types/env.ts +0 -65
  77. package/types/util.d.ts +0 -45
  78. package/util.ts +0 -257
@@ -0,0 +1,331 @@
1
+ /**
2
+ * SyncMenu 命令 - 同步菜单数据到数据库
3
+ * 说明:根据 menu.json 配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
4
+ *
5
+ * 流程:
6
+ * 1. 扫描项目根目录和所有 addon 的 menu.json 配置文件
7
+ * 2. 项目的 menu.json 优先级最高,可以覆盖 addon 的菜单配置
8
+ * 3. 文件不存在或格式错误时默认为空数组
9
+ * 4. 根据菜单的 path 字段检查是否存在
10
+ * 5. 存在则更新其他字段(name、sort、type、pid)
11
+ * 6. 不存在则新增菜单记录
12
+ * 7. 强制删除配置中不存在的菜单记录
13
+ * 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
14
+ */
15
+
16
+ import { join } from 'pathe';
17
+ import { existsSync } from 'node:fs';
18
+ import { Database } from '../lib/database.js';
19
+ import { RedisHelper } from '../lib/redisHelper.js';
20
+ import { scanAddons, getAddonDir } from 'befly-util';
21
+ import { Logger } from '../lib/logger.js';
22
+ import { projectDir } from '../paths.js';
23
+
24
+ import type { SyncMenuOptions, MenuConfig, BeflyOptions } from '../types/index.js';
25
+
26
+ /**
27
+ * 读取菜单配置文件
28
+ * 如果文件不存在或不是数组格式,返回空数组
29
+ */
30
+ async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
31
+ try {
32
+ if (!existsSync(filePath)) {
33
+ return [];
34
+ }
35
+
36
+ const content = await import(filePath, { with: { type: 'json' } });
37
+
38
+ // 验证是否为数组
39
+ if (!Array.isArray(content.default)) {
40
+ return [];
41
+ }
42
+
43
+ return content.default;
44
+ } catch (error: any) {
45
+ Logger.warn(`读取菜单配置失败 ${filePath}: ${error.message}`);
46
+ return [];
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 为 addon 菜单的 path 添加前缀
52
+ * 规则:
53
+ * 1. 所有路径必须以 / 开头
54
+ * 2. 所有路径都添加 /addon/{addonName} 前缀(包括根路径 /)
55
+ * 3. 递归处理所有层级的子菜单
56
+ * 4. 项目菜单不添加前缀
57
+ */
58
+ function addAddonPrefix(menus: MenuConfig[], addonName: string): MenuConfig[] {
59
+ return menus.map((menu) => {
60
+ const newMenu = { ...menu };
61
+
62
+ // 处理当前菜单的 path(包括根路径 /)
63
+ if (newMenu.path && newMenu.path.startsWith('/')) {
64
+ newMenu.path = `/addon/${addonName}${newMenu.path}`;
65
+ }
66
+
67
+ // 递归处理子菜单
68
+ if (newMenu.children && newMenu.children.length > 0) {
69
+ newMenu.children = addAddonPrefix(newMenu.children, addonName);
70
+ }
71
+
72
+ return newMenu;
73
+ });
74
+ }
75
+
76
+ /**
77
+ * 合并菜单配置
78
+ * 优先级:项目 menu.json > addon menu.json
79
+ * 支持三级菜单结构:父级、子级、孙级
80
+ */
81
+ function mergeMenuConfigs(allMenus: Array<{ menus: MenuConfig[]; addonName: string }>): MenuConfig[] {
82
+ /**
83
+ * 递归合并指定层级的菜单(限制最多3层)
84
+ * @param menus 待合并的菜单数组
85
+ * @param depth 当前深度(1=父级, 2=子级, 3=孙级)
86
+ * @returns 合并后的菜单数组
87
+ */
88
+ function mergeLevel(menus: MenuConfig[], depth: number = 1): MenuConfig[] {
89
+ const menuMap = new Map<string, MenuConfig>();
90
+
91
+ for (const menu of menus) {
92
+ if (!menu.path) continue;
93
+
94
+ const existing = menuMap.get(menu.path);
95
+ if (existing) {
96
+ // 合并子菜单
97
+ if (menu.children?.length > 0) {
98
+ existing.children = existing.children || [];
99
+ existing.children.push(...menu.children);
100
+ }
101
+ } else {
102
+ menuMap.set(menu.path, { ...menu });
103
+ }
104
+ }
105
+
106
+ // 递归处理子菜单(限制最多3层)
107
+ if (depth < 3) {
108
+ for (const menu of menuMap.values()) {
109
+ if (menu.children?.length > 0) {
110
+ menu.children = mergeLevel(menu.children, depth + 1);
111
+ }
112
+ }
113
+ }
114
+
115
+ return Array.from(menuMap.values());
116
+ }
117
+
118
+ // 收集所有菜单(扁平化)
119
+ const allFlatMenus = allMenus.flatMap(({ menus }) => menus);
120
+
121
+ return mergeLevel(allFlatMenus, 1);
122
+ }
123
+
124
+ /**
125
+ * 收集配置文件中所有菜单的 path(最多3级)
126
+ * 子级菜单使用独立路径
127
+ */
128
+ function collectPaths(menus: MenuConfig[]): Set<string> {
129
+ const paths = new Set<string>();
130
+
131
+ for (const menu of menus) {
132
+ if (menu.path) {
133
+ paths.add(menu.path);
134
+ }
135
+ if (menu.children && menu.children.length > 0) {
136
+ for (const child of menu.children) {
137
+ if (child.path) {
138
+ paths.add(child.path);
139
+ }
140
+ // 第三层菜单
141
+ if (child.children && child.children.length > 0) {
142
+ for (const grandChild of child.children) {
143
+ if (grandChild.path) {
144
+ paths.add(grandChild.path);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ return paths;
153
+ }
154
+
155
+ /**
156
+ * 递归同步单个菜单(限制最多3层)
157
+ * @param helper 数据库帮助类
158
+ * @param menu 菜单配置
159
+ * @param pid 父级菜单ID
160
+ * @param existingMenuMap 现有菜单映射
161
+ * @param depth 当前深度(1=父级, 2=子级, 3=孙级)
162
+ * @returns 菜单ID
163
+ */
164
+ async function syncMenuRecursive(helper: any, menu: MenuConfig, pid: number, existingMenuMap: Map<string, any>, depth: number = 1): Promise<number> {
165
+ const existing = existingMenuMap.get(menu.path || '');
166
+ let menuId: number;
167
+
168
+ if (existing) {
169
+ menuId = existing.id;
170
+
171
+ // 检查是否需要更新
172
+ const needUpdate = existing.pid !== pid || existing.name !== menu.name || existing.sort !== (menu.sort || 0);
173
+
174
+ if (needUpdate) {
175
+ await helper.updData({
176
+ table: 'addon_admin_menu',
177
+ where: { id: existing.id },
178
+ data: {
179
+ pid: pid,
180
+ name: menu.name,
181
+ sort: menu.sort || 0
182
+ }
183
+ });
184
+ }
185
+ } else {
186
+ menuId = await helper.insData({
187
+ table: 'addon_admin_menu',
188
+ data: {
189
+ pid: pid,
190
+ name: menu.name,
191
+ path: menu.path || '',
192
+ sort: menu.sort || 0
193
+ }
194
+ });
195
+ }
196
+
197
+ // 递归处理子菜单(限制最多3层)
198
+ if (depth < 3 && menu.children?.length > 0) {
199
+ for (const child of menu.children) {
200
+ await syncMenuRecursive(helper, child, menuId, existingMenuMap, depth + 1);
201
+ }
202
+ }
203
+
204
+ return menuId;
205
+ }
206
+
207
+ /**
208
+ * 同步菜单(三层结构:父级、子级、孙级)
209
+ * 子级菜单使用独立路径
210
+ */
211
+ async function syncMenus(helper: any, menus: MenuConfig[]): Promise<void> {
212
+ // 批量查询所有现有菜单,建立 path -> menu 的映射
213
+ const allExistingMenus = await helper.getAll({
214
+ table: 'addon_admin_menu',
215
+ fields: ['id', 'pid', 'name', 'path', 'sort']
216
+ });
217
+ const existingMenuMap = new Map<string, any>();
218
+ for (const menu of allExistingMenus) {
219
+ if (menu.path) {
220
+ existingMenuMap.set(menu.path, menu);
221
+ }
222
+ }
223
+
224
+ for (const menu of menus) {
225
+ try {
226
+ await syncMenuRecursive(helper, menu, 0, existingMenuMap, 1);
227
+ } catch (error: any) {
228
+ Logger.error(`同步菜单 "${menu.name}" 失败`, error.message || String(error));
229
+ throw error;
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 删除配置中不存在的菜单(强制删除)
236
+ */
237
+ async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Promise<void> {
238
+ const allRecords = await helper.getAll({
239
+ table: 'addon_admin_menu',
240
+ fields: ['id', 'path'],
241
+ where: { state$gte: 0 }
242
+ });
243
+
244
+ for (const record of allRecords) {
245
+ if (record.path && !configPaths.has(record.path)) {
246
+ await helper.delForce({
247
+ table: 'addon_admin_menu',
248
+ where: { id: record.id }
249
+ });
250
+ }
251
+ }
252
+ }
253
+
254
+ /**
255
+ * SyncMenu 命令主函数
256
+ */
257
+ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOptions = {}): Promise<void> {
258
+ try {
259
+ if (options.plan) {
260
+ Logger.debug('[计划] 同步菜单配置到数据库(plan 模式不执行)');
261
+ return;
262
+ }
263
+
264
+ // 1. 扫描所有 addon 的 menu.json 配置文件
265
+ const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
266
+
267
+ const addonNames = scanAddons();
268
+
269
+ for (const addonName of addonNames) {
270
+ const addonMenuPath = getAddonDir(addonName, 'menu.json');
271
+ if (existsSync(addonMenuPath)) {
272
+ const addonMenus = await readMenuConfig(addonMenuPath);
273
+ if (addonMenus.length > 0) {
274
+ // 为 addon 菜单添加路径前缀
275
+ const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
276
+ allMenus.push({ menus: menusWithPrefix, addonName: addonName });
277
+ }
278
+ }
279
+ }
280
+
281
+ // 2. 读取项目根目录的 menu.json(优先级最高,不添加前缀)
282
+ const projectMenuPath = join(projectDir, 'menu.json');
283
+ const projectMenus = await readMenuConfig(projectMenuPath);
284
+ if (projectMenus.length > 0) {
285
+ allMenus.push({ menus: projectMenus, addonName: 'project' });
286
+ } // 3. 合并菜单配置(项目配置优先)
287
+ const mergedMenus = mergeMenuConfigs(allMenus);
288
+
289
+ // 连接数据库(SQL + Redis)
290
+ await Database.connect();
291
+
292
+ const helper = Database.getDbHelper();
293
+
294
+ // 4. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
295
+ const exists = await helper.tableExists('addon_admin_menu');
296
+
297
+ if (!exists) {
298
+ Logger.debug('表 addon_admin_menu 不存在,跳过菜单同步(需要安装 addon-admin 组件)');
299
+ return;
300
+ }
301
+
302
+ // 5. 收集配置文件中所有菜单的 path
303
+ const configPaths = collectPaths(mergedMenus);
304
+
305
+ // 6. 同步菜单
306
+ await syncMenus(helper, mergedMenus);
307
+
308
+ // 7. 删除文件中不存在的菜单(强制删除)
309
+ await deleteObsoleteRecords(helper, configPaths);
310
+
311
+ // 8. 获取最终菜单数据(用于缓存)
312
+ const allMenusData = await helper.getAll({
313
+ table: 'addon_admin_menu',
314
+ fields: ['id', 'pid', 'name', 'path', 'sort'],
315
+ orderBy: ['sort#ASC', 'id#ASC']
316
+ });
317
+
318
+ // 9. 缓存菜单数据到 Redis
319
+ try {
320
+ const redisHelper = new RedisHelper();
321
+ await redisHelper.setObject('menus:all', allMenusData);
322
+ } catch (error: any) {
323
+ Logger.warn(`Redis 缓存菜单数据失败: ${error.message}`);
324
+ }
325
+ } catch (error: any) {
326
+ Logger.error('菜单同步失败', error);
327
+ throw error;
328
+ } finally {
329
+ await Database?.disconnect();
330
+ }
331
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Cipher 加密工具测试
3
+ */
4
+
5
+ import { describe, test, expect, beforeAll } from 'bun:test';
6
+ import { Cipher } from '../lib/cipher';
7
+ import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ describe('Cipher - 哈希功能', () => {
11
+ const testData = 'hello world';
12
+ const testDataBytes = new TextEncoder().encode(testData);
13
+
14
+ test('MD5 哈希 - 字符串输入', () => {
15
+ const result = Cipher.md5(testData);
16
+ expect(result).toBe('5eb63bbbe01eeed093cb22bb8f5acdc3');
17
+ expect(result.length).toBe(32);
18
+ });
19
+
20
+ test('MD5 哈希 - Uint8Array 输入', () => {
21
+ const result = Cipher.md5(testDataBytes);
22
+ expect(result).toBe('5eb63bbbe01eeed093cb22bb8f5acdc3');
23
+ });
24
+
25
+ test('MD5 哈希 - base64 编码', () => {
26
+ const result = Cipher.md5(testData, 'base64');
27
+ expect(typeof result).toBe('string');
28
+ expect(result.length).toBeGreaterThan(0);
29
+ });
30
+
31
+ test('SHA1 哈希', () => {
32
+ const result = Cipher.sha1(testData);
33
+ expect(result).toBe('2aae6c35c94fcfb415dbe95f408b9ce91ee846ed');
34
+ expect(result.length).toBe(40);
35
+ });
36
+
37
+ test('SHA256 哈希', () => {
38
+ const result = Cipher.sha256(testData);
39
+ expect(result).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
40
+ expect(result.length).toBe(64);
41
+ });
42
+
43
+ test('SHA512 哈希', () => {
44
+ const result = Cipher.sha512(testData);
45
+ expect(result.length).toBe(128);
46
+ expect(typeof result).toBe('string');
47
+ });
48
+
49
+ test('通用 hash 方法 - SHA256', () => {
50
+ const result = Cipher.hash('sha256', testData);
51
+ expect(result).toBe('b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9');
52
+ });
53
+
54
+ test('通用 hash 方法 - MD5', () => {
55
+ const result = Cipher.hash('md5', testData);
56
+ expect(result).toBe('5eb63bbbe01eeed093cb22bb8f5acdc3');
57
+ });
58
+ });
59
+
60
+ describe('Cipher - HMAC 签名', () => {
61
+ const key = 'secret-key';
62
+ const data = 'test data';
63
+
64
+ test('HMAC-MD5', () => {
65
+ const result = Cipher.hmacMd5(key, data);
66
+ expect(result.length).toBe(32);
67
+ expect(typeof result).toBe('string');
68
+ });
69
+
70
+ test('HMAC-SHA1', () => {
71
+ const result = Cipher.hmacSha1(key, data);
72
+ expect(result.length).toBe(40);
73
+ expect(typeof result).toBe('string');
74
+ });
75
+
76
+ test('HMAC-SHA256', () => {
77
+ const result = Cipher.hmacSha256(key, data);
78
+ expect(result.length).toBe(64);
79
+ expect(typeof result).toBe('string');
80
+ });
81
+
82
+ test('HMAC-SHA512', () => {
83
+ const result = Cipher.hmacSha512(key, data);
84
+ expect(result.length).toBe(128);
85
+ expect(typeof result).toBe('string');
86
+ });
87
+
88
+ test('通用 HMAC 方法', () => {
89
+ const result1 = Cipher.hmac('sha256', key, data);
90
+ const result2 = Cipher.hmacSha256(key, data);
91
+ expect(result1).toBe(result2);
92
+ });
93
+
94
+ test('HMAC - Uint8Array 输入', () => {
95
+ const keyBytes = new TextEncoder().encode(key);
96
+ const dataBytes = new TextEncoder().encode(data);
97
+ const result = Cipher.hmacSha256(keyBytes, dataBytes);
98
+ expect(result.length).toBe(64);
99
+ });
100
+ });
101
+
102
+ describe('Cipher - 密码加密', () => {
103
+ const password = 'MySecurePassword123!';
104
+
105
+ test('密码哈希', async () => {
106
+ const hash = await Cipher.hashPassword(password);
107
+ expect(hash).toBeDefined();
108
+ expect(hash.length).toBeGreaterThan(0);
109
+ expect(hash).not.toBe(password);
110
+ });
111
+
112
+ test('密码验证 - 正确密码', async () => {
113
+ const hash = await Cipher.hashPassword(password);
114
+ const isValid = await Cipher.verifyPassword(password, hash);
115
+ expect(isValid).toBe(true);
116
+ });
117
+
118
+ test('密码验证 - 错误密码', async () => {
119
+ const hash = await Cipher.hashPassword(password);
120
+ const isValid = await Cipher.verifyPassword('wrong-password', hash);
121
+ expect(isValid).toBe(false);
122
+ });
123
+
124
+ test('相同密码生成不同哈希', async () => {
125
+ const hash1 = await Cipher.hashPassword(password);
126
+ const hash2 = await Cipher.hashPassword(password);
127
+ expect(hash1).not.toBe(hash2);
128
+ });
129
+ });
130
+
131
+ describe('Cipher - Base64 编码', () => {
132
+ const original = 'Hello, 世界!';
133
+
134
+ test('Base64 编码', () => {
135
+ const encoded = Cipher.base64Encode(original);
136
+ expect(encoded).toBe('SGVsbG8sIOS4lueVjCE=');
137
+ });
138
+
139
+ test('Base64 解码', () => {
140
+ const encoded = 'SGVsbG8sIOS4lueVjCE=';
141
+ const decoded = Cipher.base64Decode(encoded);
142
+ expect(decoded).toBe(original);
143
+ });
144
+
145
+ test('Base64 编码解码循环', () => {
146
+ const encoded = Cipher.base64Encode(original);
147
+ const decoded = Cipher.base64Decode(encoded);
148
+ expect(decoded).toBe(original);
149
+ });
150
+
151
+ test('Base64 编码空字符串', () => {
152
+ const encoded = Cipher.base64Encode('');
153
+ expect(encoded).toBe('');
154
+ });
155
+ });
156
+
157
+ describe('Cipher - 随机字符串', () => {
158
+ test('生成指定长度的随机字符串', () => {
159
+ const result = Cipher.randomString(16);
160
+ expect(result.length).toBe(16);
161
+ expect(/^[0-9a-f]+$/.test(result)).toBe(true);
162
+ });
163
+
164
+ test('生成不同的随机字符串', () => {
165
+ const result1 = Cipher.randomString(32);
166
+ const result2 = Cipher.randomString(32);
167
+ expect(result1).not.toBe(result2);
168
+ });
169
+
170
+ test('生成奇数长度的随机字符串', () => {
171
+ const result = Cipher.randomString(15);
172
+ expect(result.length).toBe(15);
173
+ });
174
+
175
+ test('生成 0 长度字符串', () => {
176
+ const result = Cipher.randomString(0);
177
+ expect(result).toBe('');
178
+ });
179
+ });
180
+
181
+ describe('Cipher - 文件哈希', () => {
182
+ const testFilePath = join(process.cwd(), 'temp', 'cipher-test-file.txt');
183
+ const testContent = 'Test file content for hashing';
184
+
185
+ beforeAll(() => {
186
+ const tempDir = join(process.cwd(), 'temp');
187
+ if (!existsSync(tempDir)) {
188
+ mkdirSync(tempDir, { recursive: true });
189
+ }
190
+ writeFileSync(testFilePath, testContent, 'utf8');
191
+ });
192
+
193
+ test('SHA256 文件哈希', async () => {
194
+ const result = await Cipher.hashFile(testFilePath, 'sha256');
195
+ expect(result.length).toBe(64);
196
+ expect(typeof result).toBe('string');
197
+ });
198
+
199
+ test('MD5 文件哈希', async () => {
200
+ const result = await Cipher.hashFile(testFilePath, 'md5');
201
+ expect(result.length).toBe(32);
202
+ });
203
+
204
+ test('文件哈希与内容哈希一致', async () => {
205
+ const fileHash = await Cipher.hashFile(testFilePath, 'sha256');
206
+ const contentHash = Cipher.sha256(testContent);
207
+ expect(fileHash).toBe(contentHash);
208
+ });
209
+
210
+ test('文件哈希 - base64 编码', async () => {
211
+ const result = await Cipher.hashFile(testFilePath, 'sha256', 'base64');
212
+ expect(typeof result).toBe('string');
213
+ expect(result.length).toBeGreaterThan(0);
214
+ });
215
+ });
216
+
217
+ describe('Cipher - 快速哈希', () => {
218
+ const testData = 'quick hash test';
219
+
220
+ test('快速哈希 - 字符串输入', () => {
221
+ const result = Cipher.fastHash(testData);
222
+ expect(typeof result).toBe('bigint');
223
+ expect(result).toBeGreaterThan(0n);
224
+ });
225
+
226
+ test('快速哈希 - Uint8Array 输入', () => {
227
+ const bytes = new TextEncoder().encode(testData);
228
+ const result = Cipher.fastHash(bytes);
229
+ expect(typeof result).toBe('bigint');
230
+ });
231
+
232
+ test('快速哈希 - 相同输入产生相同哈希', () => {
233
+ const result1 = Cipher.fastHash(testData);
234
+ const result2 = Cipher.fastHash(testData);
235
+ expect(result1).toBe(result2);
236
+ });
237
+
238
+ test('快速哈希 - 不同 seed 产生不同结果', () => {
239
+ const result1 = Cipher.fastHash(testData, 0);
240
+ const result2 = Cipher.fastHash(testData, 1);
241
+ expect(result1).not.toBe(result2);
242
+ });
243
+
244
+ test('快速哈希 - 空字符串', () => {
245
+ const result = Cipher.fastHash('');
246
+ expect(typeof result).toBe('bigint');
247
+ });
248
+ });