befly 3.9.37 → 3.9.39

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 (155) hide show
  1. package/README.md +38 -39
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +225 -235
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +155 -153
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +7 -7
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +15 -7
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -81
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +211 -109
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +53 -47
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -54
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -66
  75. package/sync/syncMenu.ts +190 -57
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * CacheHelper 单元测试
3
3
  */
4
- import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
5
- import { CacheHelper } from '../lib/cacheHelper.js';
6
- import { Logger, setMockLogger } from '../lib/logger.js';
7
- import { RedisKeys } from 'befly-shared/redisKeys';
8
4
 
9
- import type { BeflyContext } from '../types/befly.js';
5
+ import type { BeflyContext } from "../types/befly.js";
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
8
+
9
+ import { CacheHelper } from "../lib/cacheHelper.js";
10
+ import { CacheKeys } from "../lib/cacheKeys.js";
11
+ import { setMockLogger } from "../lib/logger.js";
10
12
 
11
13
  // Mock pino logger
12
14
  const mockPino = {
@@ -18,10 +20,10 @@ const mockPino = {
18
20
  trace: mock(() => {}),
19
21
  silent: mock(() => {}),
20
22
  child: mock(() => mockPino),
21
- level: 'info'
23
+ level: "info"
22
24
  };
23
25
 
24
- describe('CacheHelper', () => {
26
+ describe("CacheHelper", () => {
25
27
  let cacheHelper: CacheHelper;
26
28
  let mockBefly: BeflyContext;
27
29
  let mockDb: any;
@@ -34,17 +36,20 @@ describe('CacheHelper', () => {
34
36
  // Mock 数据库方法
35
37
  mockDb = {
36
38
  tableExists: mock(() => Promise.resolve(true)),
37
- getAll: mock(() => Promise.resolve([]))
39
+ getAll: mock(() => Promise.resolve({ lists: [], total: 0 }))
38
40
  };
39
41
 
40
42
  // Mock Redis 方法
41
43
  mockRedis = {
42
- setObject: mock(() => Promise.resolve('OK')),
44
+ setObject: mock(() => Promise.resolve("OK")),
43
45
  getObject: mock(() => Promise.resolve(null)),
44
46
  sadd: mock(() => Promise.resolve(1)),
47
+ saddBatch: mock(() => Promise.resolve(1)),
45
48
  smembers: mock(() => Promise.resolve([])),
46
49
  sismember: mock(() => Promise.resolve(0)),
47
- del: mock(() => Promise.resolve(1))
50
+ del: mock(() => Promise.resolve(1)),
51
+ delBatch: mock(() => Promise.resolve(1)),
52
+ exists: mock(() => Promise.resolve(true))
48
53
  };
49
54
 
50
55
  // 创建 mock befly context
@@ -61,29 +66,29 @@ describe('CacheHelper', () => {
61
66
  setMockLogger(null);
62
67
  });
63
68
 
64
- describe('cacheApis', () => {
65
- it('表不存在时跳过缓存', async () => {
69
+ describe("cacheApis", () => {
70
+ it("表不存在时跳过缓存", async () => {
66
71
  mockDb.tableExists = mock(() => Promise.resolve(false));
67
72
 
68
73
  await cacheHelper.cacheApis();
69
74
 
70
- expect(mockDb.tableExists).toHaveBeenCalledWith('addon_admin_api');
75
+ expect(mockDb.tableExists).toHaveBeenCalledWith("addon_admin_api");
71
76
  expect(mockDb.getAll).not.toHaveBeenCalled();
72
77
  });
73
78
 
74
- it('正常缓存接口列表', async () => {
79
+ it("正常缓存接口列表", async () => {
75
80
  const apis = [
76
- { id: 1, name: '登录', path: '/api/login', method: 'POST' },
77
- { id: 2, name: '用户列表', path: '/api/user/list', method: 'GET' }
81
+ { id: 1, name: "登录", path: "/api/login", method: "POST" },
82
+ { id: 2, name: "用户列表", path: "/api/user/list", method: "GET" }
78
83
  ];
79
84
  mockDb.getAll = mock(() => Promise.resolve({ lists: apis, total: apis.length }));
80
85
 
81
86
  await cacheHelper.cacheApis();
82
87
 
83
- expect(mockRedis.setObject).toHaveBeenCalledWith(RedisKeys.apisAll(), apis);
88
+ expect(mockRedis.setObject).toHaveBeenCalledWith(CacheKeys.apisAll(), apis);
84
89
  });
85
90
 
86
- it('缓存失败时记录警告', async () => {
91
+ it("缓存失败时记录警告", async () => {
87
92
  mockRedis.setObject = mock(() => Promise.resolve(null));
88
93
 
89
94
  await cacheHelper.cacheApis();
@@ -91,8 +96,8 @@ describe('CacheHelper', () => {
91
96
  expect(mockPino.warn).toHaveBeenCalled();
92
97
  });
93
98
 
94
- it('异常时记录错误', async () => {
95
- mockDb.getAll = mock(() => Promise.reject(new Error('DB Error')));
99
+ it("异常时记录错误", async () => {
100
+ mockDb.getAll = mock(() => Promise.reject(new Error("DB Error")));
96
101
 
97
102
  await cacheHelper.cacheApis();
98
103
 
@@ -100,110 +105,136 @@ describe('CacheHelper', () => {
100
105
  });
101
106
  });
102
107
 
103
- describe('cacheMenus', () => {
104
- it('表不存在时跳过缓存', async () => {
108
+ describe("cacheMenus", () => {
109
+ it("表不存在时跳过缓存", async () => {
105
110
  mockDb.tableExists = mock(() => Promise.resolve(false));
106
111
 
107
112
  await cacheHelper.cacheMenus();
108
113
 
109
- expect(mockDb.tableExists).toHaveBeenCalledWith('addon_admin_menu');
114
+ expect(mockDb.tableExists).toHaveBeenCalledWith("addon_admin_menu");
110
115
  expect(mockDb.getAll).not.toHaveBeenCalled();
111
116
  });
112
117
 
113
- it('正常缓存菜单列表', async () => {
118
+ it("正常缓存菜单列表", async () => {
114
119
  const menus = [
115
- { id: 1, pid: 0, name: '首页', path: '/home', sort: 1 },
116
- { id: 2, pid: 0, name: '用户管理', path: '/user', sort: 2 }
120
+ { id: 1, pid: 0, name: "首页", path: "/home", sort: 1 },
121
+ { id: 2, pid: 0, name: "用户管理", path: "/user", sort: 2 }
117
122
  ];
118
123
  mockDb.getAll = mock(() => Promise.resolve({ lists: menus, total: menus.length }));
119
124
 
120
125
  await cacheHelper.cacheMenus();
121
126
 
122
- expect(mockRedis.setObject).toHaveBeenCalledWith(RedisKeys.menusAll(), menus);
127
+ expect(mockRedis.setObject).toHaveBeenCalledWith(CacheKeys.menusAll(), menus);
123
128
  });
124
129
  });
125
130
 
126
- describe('cacheRolePermissions', () => {
127
- it('表不存在时跳过缓存', async () => {
131
+ describe("rebuildRoleApiPermissions", () => {
132
+ it("表不存在时跳过缓存", async () => {
128
133
  mockDb.tableExists = mock((table: string) => {
129
- if (table === 'addon_admin_api') return Promise.resolve(true);
130
- if (table === 'addon_admin_role') return Promise.resolve(false);
134
+ if (table === "addon_admin_api") return Promise.resolve(true);
135
+ if (table === "addon_admin_role") return Promise.resolve(false);
131
136
  return Promise.resolve(false);
132
137
  });
133
138
 
134
- await cacheHelper.cacheRolePermissions();
139
+ await cacheHelper.rebuildRoleApiPermissions();
135
140
 
136
141
  expect(mockPino.warn).toHaveBeenCalled();
137
142
  });
138
143
 
139
- it('正常缓存角色权限', async () => {
144
+ it("正常重建角色权限(覆盖更新)", async () => {
140
145
  const roles = [
141
- { id: 1, code: 'admin', apis: '1,2,3' },
142
- { id: 2, code: 'user', apis: '1' }
146
+ { id: 1, code: "admin", apis: [1, "2", 3] },
147
+ { id: 2, code: "user", apis: [1] }
143
148
  ];
144
149
  const apis = [
145
- { id: 1, path: '/api/login', method: 'POST' },
146
- { id: 2, path: '/api/user/list', method: 'GET' },
147
- { id: 3, path: '/api/user/del', method: 'POST' }
150
+ { id: 1, path: "/api/login", method: "POST" },
151
+ { id: 2, path: "/api/user/list", method: "GET" },
152
+ { id: 3, path: "/api/user/del", method: "POST" }
148
153
  ];
149
154
 
150
155
  mockDb.getAll = mock((opts: any) => {
151
- if (opts.table === 'addon_admin_role') return Promise.resolve({ lists: roles, total: roles.length });
152
- if (opts.table === 'addon_admin_api') return Promise.resolve({ lists: apis, total: apis.length });
156
+ if (opts.table === "addon_admin_role") return Promise.resolve({ lists: roles, total: roles.length });
157
+ if (opts.table === "addon_admin_api") {
158
+ // rebuildRoleApiPermissions 会通过 where: { id$in: [...] } 分块查询
159
+ return Promise.resolve({ lists: apis, total: apis.length });
160
+ }
153
161
  return Promise.resolve({ lists: [], total: 0 });
154
162
  });
155
163
 
156
- await cacheHelper.cacheRolePermissions();
157
-
158
- // 验证批量删除调用
159
- expect(mockRedis.del).toHaveBeenCalledTimes(2);
160
-
161
- // 验证批量添加调用
162
- expect(mockRedis.sadd).toHaveBeenCalledTimes(2);
163
- expect(mockRedis.sadd).toHaveBeenCalledWith(RedisKeys.roleApis('admin'), ['POST/api/login', 'GET/api/user/list', 'POST/api/user/del']);
164
- expect(mockRedis.sadd).toHaveBeenCalledWith(RedisKeys.roleApis('user'), ['POST/api/login']);
164
+ await cacheHelper.rebuildRoleApiPermissions();
165
+
166
+ // 验证批量删除角色 key
167
+ expect(mockRedis.delBatch).toHaveBeenCalledTimes(1);
168
+ const delBatchArgs = (mockRedis.delBatch as any).mock.calls[0][0] as string[];
169
+ expect(delBatchArgs).toEqual([CacheKeys.roleApis("admin"), CacheKeys.roleApis("user")]);
170
+
171
+ // 验证批量写入
172
+ expect(mockRedis.saddBatch).toHaveBeenCalledTimes(1);
173
+ const saddBatchArgs = (mockRedis.saddBatch as any).mock.calls[0][0] as Array<{
174
+ key: string;
175
+ members: string[];
176
+ }>;
177
+ expect(saddBatchArgs).toEqual([
178
+ {
179
+ key: CacheKeys.roleApis("admin"),
180
+ members: ["GET/api/user/list", "POST/api/login", "POST/api/user/del"]
181
+ },
182
+ { key: CacheKeys.roleApis("user"), members: ["POST/api/login"] }
183
+ ]);
165
184
  });
166
185
 
167
- it('无有效角色时不执行缓存', async () => {
168
- const roles = [{ id: 1, code: 'empty', apis: null }];
169
- const apis = [{ id: 1, path: '/api/login', method: 'POST' }];
186
+ it("无权限时仍会清理旧缓存,但不写入成员", async () => {
187
+ const roles = [{ id: 1, code: "empty", apis: [] }];
188
+ const apis = [{ id: 1, path: "/api/login", method: "POST" }];
170
189
 
171
190
  mockDb.getAll = mock((opts: any) => {
172
- if (opts.table === 'addon_admin_role') return Promise.resolve({ lists: roles, total: roles.length });
173
- if (opts.table === 'addon_admin_api') return Promise.resolve({ lists: apis, total: apis.length });
191
+ if (opts.table === "addon_admin_role") return Promise.resolve({ lists: roles, total: roles.length });
192
+ if (opts.table === "addon_admin_api") return Promise.resolve({ lists: apis, total: apis.length });
174
193
  return Promise.resolve({ lists: [], total: 0 });
175
194
  });
176
195
 
177
- await cacheHelper.cacheRolePermissions();
196
+ await cacheHelper.rebuildRoleApiPermissions();
178
197
 
179
- expect(mockRedis.sadd).not.toHaveBeenCalled();
198
+ expect(mockRedis.delBatch).toHaveBeenCalledTimes(1);
199
+ expect(mockRedis.saddBatch).not.toHaveBeenCalled();
180
200
  });
201
+ });
181
202
 
182
- it('使用 Promise.all 并行执行', async () => {
183
- const roles = [
184
- { id: 1, code: 'role1', apis: '1' },
185
- { id: 2, code: 'role2', apis: '1' },
186
- { id: 3, code: 'role3', apis: '1' }
203
+ describe("refreshRoleApiPermissions", () => {
204
+ it("apiIds 为空数组时只清理缓存,不查询 API 表", async () => {
205
+ mockDb.getAll = mock(() => Promise.resolve({ lists: [], total: 0 }));
206
+
207
+ await cacheHelper.refreshRoleApiPermissions("admin", []);
208
+
209
+ expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
210
+ expect(mockDb.getAll).not.toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ table: "addon_admin_api"
213
+ })
214
+ );
215
+ });
216
+
217
+ it("apiIds 非空时使用 $in 查询并重建该角色缓存(覆盖更新)", async () => {
218
+ const apis = [
219
+ { id: 1, path: "/api/login", method: "POST" },
220
+ { id: 2, path: "/api/user/list", method: "GET" }
187
221
  ];
188
- const apis = [{ id: 1, path: '/api/test', method: 'GET' }];
189
222
 
190
223
  mockDb.getAll = mock((opts: any) => {
191
- if (opts.table === 'addon_admin_role') return Promise.resolve({ lists: roles, total: roles.length });
192
- if (opts.table === 'addon_admin_api') return Promise.resolve({ lists: apis, total: apis.length });
224
+ if (opts.table === "addon_admin_api") return Promise.resolve({ lists: apis, total: apis.length });
193
225
  return Promise.resolve({ lists: [], total: 0 });
194
226
  });
195
227
 
196
- await cacheHelper.cacheRolePermissions();
228
+ await cacheHelper.refreshRoleApiPermissions("admin", [1, 2]);
197
229
 
198
- // 3 个角色应该有 3 次删除和 3 次添加操作
199
- expect(mockRedis.del).toHaveBeenCalledTimes(3);
200
- expect(mockRedis.sadd).toHaveBeenCalledTimes(3);
230
+ expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
231
+ expect(mockRedis.sadd).toHaveBeenCalledWith(CacheKeys.roleApis("admin"), ["POST/api/login", "GET/api/user/list"]);
201
232
  });
202
233
  });
203
234
 
204
- describe('getApis', () => {
205
- it('返回缓存的接口列表', async () => {
206
- const apis = [{ id: 1, name: '登录' }];
235
+ describe("getApis", () => {
236
+ it("返回缓存的接口列表", async () => {
237
+ const apis = [{ id: 1, name: "登录" }];
207
238
  mockRedis.getObject = mock(() => Promise.resolve(apis));
208
239
 
209
240
  const result = await cacheHelper.getApis();
@@ -211,7 +242,7 @@ describe('CacheHelper', () => {
211
242
  expect(result).toEqual(apis);
212
243
  });
213
244
 
214
- it('缓存不存在时返回空数组', async () => {
245
+ it("缓存不存在时返回空数组", async () => {
215
246
  mockRedis.getObject = mock(() => Promise.resolve(null));
216
247
 
217
248
  const result = await cacheHelper.getApis();
@@ -220,9 +251,9 @@ describe('CacheHelper', () => {
220
251
  });
221
252
  });
222
253
 
223
- describe('getMenus', () => {
224
- it('返回缓存的菜单列表', async () => {
225
- const menus = [{ id: 1, name: '首页' }];
254
+ describe("getMenus", () => {
255
+ it("返回缓存的菜单列表", async () => {
256
+ const menus = [{ id: 1, name: "首页" }];
226
257
  mockRedis.getObject = mock(() => Promise.resolve(menus));
227
258
 
228
259
  const result = await cacheHelper.getMenus();
@@ -230,7 +261,7 @@ describe('CacheHelper', () => {
230
261
  expect(result).toEqual(menus);
231
262
  });
232
263
 
233
- it('缓存不存在时返回空数组', async () => {
264
+ it("缓存不存在时返回空数组", async () => {
234
265
  mockRedis.getObject = mock(() => Promise.resolve(null));
235
266
 
236
267
  const result = await cacheHelper.getMenus();
@@ -239,89 +270,89 @@ describe('CacheHelper', () => {
239
270
  });
240
271
  });
241
272
 
242
- describe('getRolePermissions', () => {
243
- it('返回角色的权限列表', async () => {
244
- const permissions = ['POST/api/login', 'GET/api/user/list'];
273
+ describe("getRolePermissions", () => {
274
+ it("返回角色的权限列表", async () => {
275
+ const permissions = ["POST/api/login", "GET/api/user/list"];
245
276
  mockRedis.smembers = mock(() => Promise.resolve(permissions));
246
277
 
247
- const result = await cacheHelper.getRolePermissions('admin');
278
+ const result = await cacheHelper.getRolePermissions("admin");
248
279
 
249
- expect(mockRedis.smembers).toHaveBeenCalledWith(RedisKeys.roleApis('admin'));
280
+ expect(mockRedis.smembers).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
250
281
  expect(result).toEqual(permissions);
251
282
  });
252
283
 
253
- it('权限不存在时返回空数组', async () => {
284
+ it("权限不存在时返回空数组", async () => {
254
285
  mockRedis.smembers = mock(() => Promise.resolve(null));
255
286
 
256
- const result = await cacheHelper.getRolePermissions('unknown');
287
+ const result = await cacheHelper.getRolePermissions("unknown");
257
288
 
258
289
  expect(result).toEqual([]);
259
290
  });
260
291
  });
261
292
 
262
- describe('checkRolePermission', () => {
263
- it('有权限时返回 true', async () => {
293
+ describe("checkRolePermission", () => {
294
+ it("有权限时返回 true", async () => {
264
295
  mockRedis.sismember = mock(() => Promise.resolve(true));
265
296
 
266
- const result = await cacheHelper.checkRolePermission('admin', 'POST/api/login');
297
+ const result = await cacheHelper.checkRolePermission("admin", "POST/api/login");
267
298
 
268
- expect(mockRedis.sismember).toHaveBeenCalledWith(RedisKeys.roleApis('admin'), 'POST/api/login');
299
+ expect(mockRedis.sismember).toHaveBeenCalledWith(CacheKeys.roleApis("admin"), "POST/api/login");
269
300
  expect(result).toBe(true);
270
301
  });
271
302
 
272
- it('无权限时返回 false', async () => {
303
+ it("无权限时返回 false", async () => {
273
304
  mockRedis.sismember = mock(() => Promise.resolve(false));
274
305
 
275
- const result = await cacheHelper.checkRolePermission('user', 'POST/api/admin/del');
306
+ const result = await cacheHelper.checkRolePermission("user", "POST/api/admin/del");
276
307
 
277
308
  expect(result).toBe(false);
278
309
  });
279
310
  });
280
311
 
281
- describe('deleteRolePermissions', () => {
282
- it('删除成功返回 true', async () => {
312
+ describe("deleteRolePermissions", () => {
313
+ it("删除成功返回 true", async () => {
283
314
  mockRedis.del = mock(() => Promise.resolve(1));
284
315
 
285
- const result = await cacheHelper.deleteRolePermissions('admin');
316
+ const result = await cacheHelper.deleteRolePermissions("admin");
286
317
 
287
- expect(mockRedis.del).toHaveBeenCalledWith(RedisKeys.roleApis('admin'));
318
+ expect(mockRedis.del).toHaveBeenCalledWith(CacheKeys.roleApis("admin"));
288
319
  expect(result).toBe(true);
289
320
  });
290
321
 
291
- it('不存在时返回 false', async () => {
322
+ it("不存在时返回 false", async () => {
292
323
  mockRedis.del = mock(() => Promise.resolve(0));
293
324
 
294
- const result = await cacheHelper.deleteRolePermissions('unknown');
325
+ const result = await cacheHelper.deleteRolePermissions("unknown");
295
326
 
296
327
  expect(result).toBe(false);
297
328
  });
298
329
  });
299
330
 
300
- describe('cacheAll', () => {
301
- it('按顺序调用三个缓存方法', async () => {
331
+ describe("cacheAll", () => {
332
+ it("按顺序调用三个缓存方法", async () => {
302
333
  const callOrder: string[] = [];
303
334
 
304
335
  // 使用 spy 记录调用顺序
305
336
  const originalCacheApis = cacheHelper.cacheApis.bind(cacheHelper);
306
337
  const originalCacheMenus = cacheHelper.cacheMenus.bind(cacheHelper);
307
- const originalCacheRolePermissions = cacheHelper.cacheRolePermissions.bind(cacheHelper);
338
+ const originalRebuildRoleApiPermissions = cacheHelper.rebuildRoleApiPermissions.bind(cacheHelper);
308
339
 
309
340
  cacheHelper.cacheApis = async () => {
310
- callOrder.push('apis');
341
+ callOrder.push("apis");
311
342
  await originalCacheApis();
312
343
  };
313
344
  cacheHelper.cacheMenus = async () => {
314
- callOrder.push('menus');
345
+ callOrder.push("menus");
315
346
  await originalCacheMenus();
316
347
  };
317
- cacheHelper.cacheRolePermissions = async () => {
318
- callOrder.push('permissions');
319
- await originalCacheRolePermissions();
348
+ cacheHelper.rebuildRoleApiPermissions = async () => {
349
+ callOrder.push("permissions");
350
+ await originalRebuildRoleApiPermissions();
320
351
  };
321
352
 
322
353
  await cacheHelper.cacheAll();
323
354
 
324
- expect(callOrder).toEqual(['apis', 'menus', 'permissions']);
355
+ expect(callOrder).toEqual(["apis", "menus", "permissions"]);
325
356
  });
326
357
  });
327
358
  });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * CacheKeys 测试
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+
7
+ import { CacheKeys } from "../lib/cacheKeys.js";
8
+
9
+ describe("CacheKeys - Key 生成函数", () => {
10
+ test("apisAll - 返回固定的接口缓存键", () => {
11
+ expect(CacheKeys.apisAll()).toBe("befly:apis:all");
12
+ });
13
+
14
+ test("menusAll - 返回固定的菜单缓存键", () => {
15
+ expect(CacheKeys.menusAll()).toBe("befly:menus:all");
16
+ });
17
+
18
+ test("roleInfo - 返回带角色代码的缓存键", () => {
19
+ expect(CacheKeys.roleInfo("admin")).toBe("befly:role:info:admin");
20
+ expect(CacheKeys.roleInfo("user")).toBe("befly:role:info:user");
21
+ });
22
+
23
+ test("roleApis - 返回带角色代码的权限缓存键", () => {
24
+ expect(CacheKeys.roleApis("admin")).toBe("befly:role:apis:admin");
25
+ expect(CacheKeys.roleApis("guest")).toBe("befly:role:apis:guest");
26
+ });
27
+
28
+ test("tableColumns - 返回带表名的结构缓存键", () => {
29
+ expect(CacheKeys.tableColumns("user")).toBe("befly:table:columns:user");
30
+ expect(CacheKeys.tableColumns("addon_admin_role")).toBe("befly:table:columns:addon_admin_role");
31
+ });
32
+
33
+ test("roleInfo - 特殊字符处理", () => {
34
+ expect(CacheKeys.roleInfo("super-admin")).toBe("befly:role:info:super-admin");
35
+ expect(CacheKeys.roleInfo("role_1")).toBe("befly:role:info:role_1");
36
+ });
37
+
38
+ test("tableColumns - 空字符串", () => {
39
+ expect(CacheKeys.tableColumns("")).toBe("befly:table:columns:");
40
+ });
41
+ });