system-config.routes.integration.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. import { describe, it, expect, beforeEach, vi } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import {
  4. IntegrationTestDatabase,
  5. setupIntegrationDatabaseHooksWithEntities
  6. } from '@d8d/shared-test-util';
  7. import { systemConfigRoutesMt } from '../../src/routes/system-config.routes.mt';
  8. import { SystemConfigMt } from '../../src/entities/system-config.entity.mt';
  9. import { SystemConfigServiceMt } from '../../src/services/system-config.service.mt';
  10. import { UserEntityMt, RoleMt } from '@d8d/core-module-mt/user-module-mt';
  11. import { FileMt } from '@d8d/core-module-mt/file-module-mt';
  12. import { TestDataFactory } from '../utils/integration-test-db';
  13. import { AuthService } from '@d8d/core-module-mt/auth-module-mt';
  14. import { UserServiceMt } from '@d8d/core-module-mt/user-module-mt';
  15. import { redisUtil } from '@d8d/shared-utils';
  16. // 设置集成测试钩子
  17. setupIntegrationDatabaseHooksWithEntities([SystemConfigMt, UserEntityMt, RoleMt, FileMt])
  18. describe('系统配置路由API集成测试 (使用hono/testing)', () => {
  19. let client: ReturnType<typeof testClient<typeof systemConfigRoutesMt>>;
  20. let authService: AuthService;
  21. let userService: UserServiceMt;
  22. let testToken: string;
  23. let testUser: any;
  24. beforeEach(async () => {
  25. // 创建测试客户端
  26. client = testClient(systemConfigRoutesMt);
  27. // 获取数据源
  28. const dataSource = await IntegrationTestDatabase.getDataSource();
  29. if (!dataSource) throw new Error('Database not initialized');
  30. // 初始化服务
  31. userService = new UserServiceMt(dataSource);
  32. authService = new AuthService(userService);
  33. // 创建测试用户并生成token
  34. testUser = await TestDataFactory.createTestUser(dataSource, {
  35. username: 'testuser_systemconfig',
  36. password: 'TestPassword123!',
  37. email: 'testuser_systemconfig@example.com'
  38. });
  39. // 生成测试用户的token
  40. testToken = authService.generateToken(testUser);
  41. });
  42. describe('CRUD Operations', () => {
  43. it('应该成功创建系统配置', async () => {
  44. const configData = {
  45. configKey: 'app.login.enabled',
  46. configValue: 'true',
  47. description: '控制小程序登录功能是否开启'
  48. };
  49. const response = await client.index.$post({
  50. json: configData
  51. }, {
  52. headers: {
  53. 'Authorization': `Bearer ${testToken}`
  54. }
  55. });
  56. expect(response.status).toBe(201);
  57. const result = await response.json();
  58. if ('configKey' in result) {
  59. expect(result.configKey).toBe(configData.configKey);
  60. expect(result.configValue).toBe(configData.configValue);
  61. }
  62. });
  63. it('应该根据ID获取系统配置', async () => {
  64. // 先创建配置
  65. const dataSource = await IntegrationTestDatabase.getDataSource();
  66. if (!dataSource) throw new Error('Database not initialized');
  67. const systemConfigService = new SystemConfigServiceMt(dataSource);
  68. const config = await systemConfigService.create({
  69. configKey: 'app.payment.enabled',
  70. configValue: 'true',
  71. description: '控制支付功能是否开启',
  72. tenantId: testUser.tenantId
  73. } as SystemConfigMt);
  74. const response = await client[':id'].$get({
  75. param: { id: config.id }
  76. }, {
  77. headers: {
  78. 'Authorization': `Bearer ${testToken}`
  79. }
  80. });
  81. expect(response.status).toBe(200);
  82. const result = await response.json();
  83. if ('id' in result) {
  84. expect(result.id).toBe(config.id);
  85. expect(result.configKey).toBe(config.configKey);
  86. }
  87. });
  88. it('应该更新系统配置', async () => {
  89. // 先创建配置
  90. const dataSource = await IntegrationTestDatabase.getDataSource();
  91. if (!dataSource) throw new Error('Database not initialized');
  92. const systemConfigService = new SystemConfigServiceMt(dataSource);
  93. const config = await systemConfigService.create({
  94. configKey: 'app.notification.enabled',
  95. configValue: 'true',
  96. description: '控制通知功能是否开启',
  97. tenantId: testUser.tenantId
  98. } as SystemConfigMt);
  99. const updateData = {
  100. configValue: 'false',
  101. description: '通知功能已关闭'
  102. };
  103. const response = await client[':id'].$put({
  104. param: { id: config.id },
  105. json: updateData
  106. }, {
  107. headers: {
  108. 'Authorization': `Bearer ${testToken}`
  109. }
  110. });
  111. expect(response.status).toBe(200);
  112. const result = await response.json();
  113. if ('configValue' in result) {
  114. expect(result.configValue).toBe(updateData.configValue);
  115. expect(result.description).toBe(updateData.description);
  116. }
  117. });
  118. it('应该删除系统配置', async () => {
  119. // 先创建配置
  120. const dataSource = await IntegrationTestDatabase.getDataSource();
  121. if (!dataSource) throw new Error('Database not initialized');
  122. const systemConfigService = new SystemConfigServiceMt(dataSource);
  123. const config = await systemConfigService.create({
  124. configKey: 'app.analytics.enabled',
  125. configValue: 'true',
  126. description: '控制分析功能是否开启',
  127. tenantId: testUser.tenantId
  128. } as SystemConfigMt);
  129. const response = await client[':id'].$delete({
  130. param: { id: config.id }
  131. }, {
  132. headers: {
  133. 'Authorization': `Bearer ${testToken}`
  134. }
  135. });
  136. expect(response.status).toBe(204);
  137. // 验证配置已被删除
  138. const deletedConfig = await systemConfigService.getById(config.id);
  139. expect(deletedConfig).toBeNull();
  140. });
  141. it('应该列出系统配置列表', async () => {
  142. // 创建多个配置
  143. const dataSource = await IntegrationTestDatabase.getDataSource();
  144. if (!dataSource) throw new Error('Database not initialized');
  145. const systemConfigService = new SystemConfigServiceMt(dataSource);
  146. await systemConfigService.create({
  147. configKey: 'app.feature1.enabled',
  148. configValue: 'true',
  149. tenantId: testUser.tenantId
  150. } as SystemConfigMt);
  151. await systemConfigService.create({
  152. configKey: 'app.feature2.enabled',
  153. configValue: 'false',
  154. tenantId: testUser.tenantId
  155. } as SystemConfigMt);
  156. const response = await client.index.$get({
  157. query: {}
  158. }, {
  159. headers: {
  160. 'Authorization': `Bearer ${testToken}`
  161. }
  162. });
  163. expect(response.status).toBe(200);
  164. const result = await response.json();
  165. if ('data' in result) {
  166. expect(result.data).toHaveLength(2);
  167. }
  168. });
  169. });
  170. describe('Multi-tenant Isolation', () => {
  171. let tenant1User: any;
  172. let tenant2User: any;
  173. let tenant1Token: string;
  174. let tenant2Token: string;
  175. beforeEach(async () => {
  176. const dataSource = await IntegrationTestDatabase.getDataSource();
  177. if (!dataSource) throw new Error('Database not initialized');
  178. // 创建租户1的用户
  179. tenant1User = await TestDataFactory.createTestUser(dataSource, {
  180. username: 'tenant1_user_systemconfig',
  181. password: 'TestPassword123!',
  182. email: 'tenant1_systemconfig@example.com',
  183. tenantId: 1
  184. });
  185. // 创建租户2的用户
  186. tenant2User = await TestDataFactory.createTestUser(dataSource, {
  187. username: 'tenant2_user_systemconfig',
  188. password: 'TestPassword123!',
  189. email: 'tenant2_systemconfig@example.com',
  190. tenantId: 2
  191. });
  192. // 生成租户用户的token
  193. tenant1Token = authService.generateToken(tenant1User);
  194. tenant2Token = authService.generateToken(tenant2User);
  195. });
  196. it('应该按租户隔离配置', async () => {
  197. const dataSource = await IntegrationTestDatabase.getDataSource();
  198. if (!dataSource) throw new Error('Database not initialized');
  199. const systemConfigService = new SystemConfigServiceMt(dataSource);
  200. // 为租户1创建配置
  201. await systemConfigService.create({
  202. configKey: 'app.shared.config',
  203. configValue: 'tenant1-value',
  204. tenantId: 1
  205. } as SystemConfigMt);
  206. // 为租户2创建配置
  207. await systemConfigService.create({
  208. configKey: 'app.shared.config',
  209. configValue: 'tenant2-value',
  210. tenantId: 2
  211. } as SystemConfigMt);
  212. // 查询租户1的配置
  213. const response1 = await client.index.$get({
  214. query: {}
  215. }, {
  216. headers: {
  217. 'Authorization': `Bearer ${tenant1Token}`
  218. }
  219. });
  220. expect(response1.status).toBe(200);
  221. const result1 = await response1.json();
  222. if ('data' in result1) {
  223. expect(result1.data).toHaveLength(1);
  224. expect(result1.data[0].configValue).toBe('tenant1-value');
  225. }
  226. // 查询租户2的配置
  227. const response2 = await client.index.$get({
  228. query: {}
  229. }, {
  230. headers: {
  231. 'Authorization': `Bearer ${tenant2Token}`
  232. }
  233. });
  234. expect(response2.status).toBe(200);
  235. const result2 = await response2.json();
  236. if ('data' in result2) {
  237. expect(result2.data).toHaveLength(1);
  238. expect(result2.data[0].configValue).toBe('tenant2-value');
  239. }
  240. });
  241. });
  242. describe('Data Validation', () => {
  243. it('应该验证必填字段', async () => {
  244. const invalidData = {
  245. configKey: '', // 空字符串
  246. configValue: 'true'
  247. };
  248. const response = await client.index.$post({
  249. json: invalidData
  250. }, {
  251. headers: {
  252. 'Authorization': `Bearer ${testToken}`
  253. }
  254. });
  255. expect(response.status).toBe(400);
  256. });
  257. it('应该验证字段长度', async () => {
  258. const invalidData = {
  259. configKey: 'a'.repeat(256), // 超过255字符
  260. configValue: 'true'
  261. };
  262. const response = await client.index.$post({
  263. json: invalidData
  264. }, {
  265. headers: {
  266. 'Authorization': `Bearer ${testToken}`
  267. }
  268. });
  269. expect(response.status).toBe(400);
  270. });
  271. });
  272. describe('自定义路由缓存刷新验证', () => {
  273. beforeEach(async () => {
  274. // 清除测试缓存
  275. await redisUtil.deleteSystemConfig(testUser.tenantId, 'app.login.enabled');
  276. await redisUtil.deleteSystemConfig(testUser.tenantId, 'app.payment.enabled');
  277. });
  278. it('应该通过自定义创建路由刷新缓存', async () => {
  279. const configData = {
  280. configKey: 'app.login.enabled',
  281. configValue: 'true',
  282. description: '控制小程序登录功能是否开启'
  283. };
  284. // 验证缓存初始为空
  285. const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, configData.configKey);
  286. expect(initialCache).toBeNull();
  287. // 通过自定义路由创建配置
  288. const response = await client.index.$post({
  289. json: configData
  290. }, {
  291. headers: {
  292. 'Authorization': `Bearer ${testToken}`
  293. }
  294. });
  295. expect(response.status).toBe(201);
  296. const result = await response.json() as any;
  297. expect(result.configKey).toBe(configData.configKey);
  298. expect(result.configValue).toBe(configData.configValue);
  299. // 验证缓存已被刷新(为空,因为setConfig会删除缓存)
  300. const afterCreateCache = await redisUtil.getSystemConfig(testUser.tenantId, configData.configKey);
  301. expect(afterCreateCache).toBeNull();
  302. });
  303. it('应该通过自定义更新路由刷新缓存', async () => {
  304. // 先创建配置
  305. const dataSource = await IntegrationTestDatabase.getDataSource();
  306. if (!dataSource) throw new Error('Database not initialized');
  307. const systemConfigService = new SystemConfigServiceMt(dataSource);
  308. const config = await systemConfigService.create({
  309. configKey: 'app.payment.enabled',
  310. configValue: 'true',
  311. description: '控制支付功能是否开启',
  312. tenantId: testUser.tenantId
  313. } as any);
  314. // 设置缓存
  315. await redisUtil.setSystemConfig(testUser.tenantId, config.configKey, 'cached-value', 3600);
  316. const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
  317. expect(initialCache).toBe('cached-value');
  318. // 通过自定义路由更新配置
  319. const updateData = {
  320. configValue: 'false',
  321. description: '支付功能已关闭'
  322. };
  323. const response = await client[':id'].$put({
  324. param: { id: config.id },
  325. json: updateData
  326. }, {
  327. headers: {
  328. 'Authorization': `Bearer ${testToken}`
  329. }
  330. });
  331. expect(response.status).toBe(200);
  332. const result = await response.json() as any;
  333. expect(result.configValue).toBe(updateData.configValue);
  334. // 验证缓存已被刷新
  335. const afterUpdateCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
  336. expect(afterUpdateCache).toBeNull();
  337. });
  338. it('应该通过自定义删除路由刷新缓存', async () => {
  339. // 先创建配置
  340. const dataSource = await IntegrationTestDatabase.getDataSource();
  341. if (!dataSource) throw new Error('Database not initialized');
  342. const systemConfigService = new SystemConfigServiceMt(dataSource);
  343. const config = await systemConfigService.create({
  344. configKey: 'app.analytics.enabled',
  345. configValue: 'true',
  346. description: '控制分析功能是否开启',
  347. tenantId: testUser.tenantId
  348. } as any);
  349. // 设置缓存
  350. await redisUtil.setSystemConfig(testUser.tenantId, config.configKey, 'cached-value', 3600);
  351. const initialCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
  352. expect(initialCache).toBe('cached-value');
  353. // 通过自定义路由删除配置
  354. const response = await client[':id'].$delete({
  355. param: { id: config.id }
  356. }, {
  357. headers: {
  358. 'Authorization': `Bearer ${testToken}`
  359. }
  360. });
  361. expect(response.status).toBe(204);
  362. // 验证缓存已被刷新
  363. const afterDeleteCache = await redisUtil.getSystemConfig(testUser.tenantId, config.configKey);
  364. expect(afterDeleteCache).toBeNull();
  365. });
  366. });
  367. describe('自定义路由多租户隔离验证', () => {
  368. let tenant1User: any;
  369. let tenant2User: any;
  370. let tenant1Token: string;
  371. let tenant2Token: string;
  372. beforeEach(async () => {
  373. const dataSource = await IntegrationTestDatabase.getDataSource();
  374. if (!dataSource) throw new Error('Database not initialized');
  375. // 创建租户1的用户
  376. tenant1User = await TestDataFactory.createTestUser(dataSource, {
  377. username: 'tenant1_customroutes',
  378. password: 'TestPassword123!',
  379. email: 'tenant1_customroutes@example.com',
  380. tenantId: 1
  381. });
  382. // 创建租户2的用户
  383. tenant2User = await TestDataFactory.createTestUser(dataSource, {
  384. username: 'tenant2_customroutes',
  385. password: 'TestPassword123!',
  386. email: 'tenant2_customroutes@example.com',
  387. tenantId: 2
  388. });
  389. // 生成租户用户的token
  390. tenant1Token = authService.generateToken(tenant1User);
  391. tenant2Token = authService.generateToken(tenant2User);
  392. // 清除测试缓存
  393. await redisUtil.deleteSystemConfig(1, 'app.shared.config');
  394. await redisUtil.deleteSystemConfig(2, 'app.shared.config');
  395. });
  396. it('应该确保多租户缓存隔离', async () => {
  397. const configData = {
  398. configKey: 'app.shared.config',
  399. configValue: 'tenant1-value',
  400. description: '共享配置'
  401. };
  402. // 租户1创建配置
  403. const response1 = await client.index.$post({
  404. json: configData
  405. }, {
  406. headers: {
  407. 'Authorization': `Bearer ${tenant1Token}`
  408. }
  409. });
  410. expect(response1.status).toBe(201);
  411. // 租户2创建配置
  412. const response2 = await client.index.$post({
  413. json: { ...configData, configValue: 'tenant2-value' }
  414. }, {
  415. headers: {
  416. 'Authorization': `Bearer ${tenant2Token}`
  417. }
  418. });
  419. expect(response2.status).toBe(201);
  420. // 验证租户1的缓存操作不影响租户2
  421. const tenant1Cache = await redisUtil.getSystemConfig(1, configData.configKey);
  422. const tenant2Cache = await redisUtil.getSystemConfig(2, configData.configKey);
  423. // 两个租户的缓存都应该是空的,因为setConfig会删除缓存
  424. expect(tenant1Cache).toBeNull();
  425. expect(tenant2Cache).toBeNull();
  426. });
  427. it('应该验证租户ID正确提取', async () => {
  428. const configData = {
  429. configKey: 'app.tenant.specific',
  430. configValue: 'tenant-specific-value',
  431. description: '租户特定配置'
  432. };
  433. // 租户1创建配置
  434. const response = await client.index.$post({
  435. json: configData
  436. }, {
  437. headers: {
  438. 'Authorization': `Bearer ${tenant1Token}`
  439. }
  440. });
  441. expect(response.status).toBe(201);
  442. const result = await response.json() as any;
  443. // 验证配置属于正确的租户
  444. expect(result.tenantId).toBe(1);
  445. // 验证租户2无法访问租户1的配置
  446. const dataSource = await IntegrationTestDatabase.getDataSource();
  447. if (!dataSource) throw new Error('Database not initialized');
  448. const systemConfigService = new SystemConfigServiceMt(dataSource);
  449. const tenant2Configs = await systemConfigService.getAllConfigs(2);
  450. const tenant2HasConfig = tenant2Configs.some(config => config.configKey === configData.configKey);
  451. expect(tenant2HasConfig).toBe(false);
  452. });
  453. });
  454. });