unified-advertisement-auth.integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. import { describe, it, expect, beforeEach, beforeAll, afterAll } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import {
  4. IntegrationTestDatabase,
  5. TestDataFactory
  6. } from '../utils/integration-test-db';
  7. import { UserEntityMt, UserServiceMt } from '@d8d/user-module-mt';
  8. // 导入server包的api以确保数据源初始化,同时获取统一广告路由的类型
  9. import { adminUnifiedAdvertisementApiRoutes, adminUnifiedAdvertisementTypeApiRoutes, unifiedAdvertisementApiRoutes } from '../../src/api';
  10. import { AuthService } from '@d8d/auth-module-mt';
  11. import { UnifiedAdvertisement, UnifiedAdvertisementType } from '@d8d/unified-advertisements-module';
  12. describe('统一广告管理员权限集成测试', () => {
  13. let adminClient: ReturnType<typeof testClient<typeof adminUnifiedAdvertisementApiRoutes>>['api']['v1']['admin']['unified-advertisements'];
  14. let adminTypeClient: ReturnType<typeof testClient<typeof adminUnifiedAdvertisementTypeApiRoutes>>['api']['v1']['admin']['unified-advertisement-types'];
  15. let userClient: ReturnType<typeof testClient<typeof unifiedAdvertisementApiRoutes>>['api']['v1']['advertisements'];
  16. let userTypeClient: ReturnType<typeof testClient<typeof unifiedAdvertisementApiRoutes>>['api']['v1']['advertisement-types'];
  17. let authService: AuthService;
  18. let userService: UserServiceMt;
  19. let superAdminToken: string;
  20. let regularUserToken: string;
  21. let tenantUserToken: string;
  22. // 使用 beforeAll 和 afterAll 而不是 beforeEach/afterEach,避免每次测试都销毁和重新创建数据源
  23. beforeAll(async () => {
  24. await IntegrationTestDatabase.getDataSource();
  25. });
  26. afterAll(async () => {
  27. await IntegrationTestDatabase.cleanup();
  28. });
  29. beforeEach(async () => {
  30. // 创建测试客户端 - 使用server包注册后的路由
  31. adminClient = testClient(adminUnifiedAdvertisementApiRoutes).api.v1.admin['unified-advertisements'];
  32. adminTypeClient = testClient(adminUnifiedAdvertisementTypeApiRoutes).api.v1.admin['unified-advertisement-types'];
  33. userClient = testClient(unifiedAdvertisementApiRoutes).api.v1.advertisements;
  34. userTypeClient = testClient(unifiedAdvertisementApiRoutes).api.v1['advertisement-types'];
  35. // 获取数据源
  36. const dataSource = await IntegrationTestDatabase.getDataSource();
  37. if (!dataSource) throw new Error('Database not initialized');
  38. // 清理测试数据(用户、广告、广告类型)
  39. const userRepository = dataSource.getRepository(UserEntityMt);
  40. await userRepository.delete({ username: 'superadmin' });
  41. await userRepository.delete({ username: 'regularuser' });
  42. await userRepository.delete({ username: 'tenantuser' });
  43. // 清理广告和广告类型测试数据(使用repository delete方法)
  44. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  45. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  46. // 先删除广告(因为它们引用广告类型)
  47. const ads = await adRepository.find();
  48. for (const ad of ads) {
  49. await adRepository.remove(ad);
  50. }
  51. // 再删除广告类型
  52. const adTypes = await adTypeRepository.find();
  53. for (const adType of adTypes) {
  54. await adTypeRepository.remove(adType);
  55. }
  56. // 初始化服务
  57. userService = new UserServiceMt(dataSource);
  58. authService = new AuthService(userService);
  59. // 创建超级管理员 (ID=1, tenantId=1)
  60. // 先删除可能存在的超级管理员
  61. await userRepository.delete({ username: 'superadmin' });
  62. const superAdmin = await TestDataFactory.createTestUser(dataSource, {
  63. username: 'superadmin',
  64. password: 'TestPassword123!',
  65. email: 'superadmin@example.com',
  66. tenantId: 1
  67. });
  68. // 手动设置ID为1以确保是超级管理员
  69. superAdmin.id = 1;
  70. // 使用query builder直接更新ID,避免save时的ID冲突
  71. await dataSource.createQueryBuilder()
  72. .update(UserEntityMt)
  73. .set({ id: 1 })
  74. .where('username = :username', { username: 'superadmin' })
  75. .execute();
  76. // 重新查询用户以确保ID正确
  77. const superAdminWithId = await userRepository.findOne({ where: { username: 'superadmin' } });
  78. if (!superAdminWithId || superAdminWithId.id !== 1) {
  79. throw new Error('超级管理员ID设置失败');
  80. }
  81. superAdminToken = authService.generateToken(superAdminWithId);
  82. // 创建普通管理员用户 (ID>1, tenantId=1)
  83. const regularUser = await TestDataFactory.createTestUser(dataSource, {
  84. username: 'regularuser',
  85. password: 'TestPassword123!',
  86. email: 'regular@example.com',
  87. tenantId: 1
  88. });
  89. regularUserToken = authService.generateToken(regularUser);
  90. // 创建普通租户用户 (tenantId>1)
  91. const tenantUser = await TestDataFactory.createTestUser(dataSource, {
  92. username: 'tenantuser',
  93. password: 'TestPassword123!',
  94. email: 'tenant@example.com',
  95. tenantId: 2
  96. });
  97. tenantUserToken = authService.generateToken(tenantUser);
  98. });
  99. describe('管理员广告API权限控制', () => {
  100. it('超级管理员(ID=1)应该能访问管理员广告列表API', async () => {
  101. const response = await adminClient.$get(undefined, {
  102. headers: {
  103. 'Authorization': `Bearer ${superAdminToken}`,
  104. 'X-Tenant-ID': '1',
  105. 'X-User-ID': '1'
  106. }
  107. });
  108. // 超级管理员应该能够访问 (200) 或至少通过认证检查 (不是401/403)
  109. expect([200, 404]).toContain(response.status);
  110. if (response.status === 200) {
  111. const data = await response.json();
  112. // API返回格式: { code, message, data: { list, total } }
  113. expect(data).toHaveProperty('data');
  114. expect(data.data).toHaveProperty('list');
  115. expect(Array.isArray(data.data.list)).toBeTruthy();
  116. }
  117. });
  118. it('普通管理员(ID>1)不应该能访问管理员广告列表API', async () => {
  119. const response = await adminClient.$get(undefined, {
  120. headers: {
  121. 'Authorization': `Bearer ${regularUserToken}`,
  122. 'X-Tenant-ID': '1',
  123. 'X-User-ID': '2'
  124. }
  125. });
  126. // 普通管理员应该被拒绝访问
  127. expect([401, 403]).toContain(response.status);
  128. });
  129. it('普通租户用户(tenantId>1)不应该能访问管理员广告列表API', async () => {
  130. const response = await adminClient.$get(undefined, {
  131. headers: {
  132. 'Authorization': `Bearer ${tenantUserToken}`,
  133. 'X-Tenant-ID': '2',
  134. 'X-User-ID': '3'
  135. }
  136. });
  137. // 普通租户用户应该被拒绝访问
  138. expect([401, 403]).toContain(response.status);
  139. });
  140. it('未认证用户不应该能访问管理员广告列表API', async () => {
  141. const response = await adminClient.$get();
  142. // 未认证用户应该被拒绝
  143. expect(response.status).toBe(401);
  144. });
  145. });
  146. describe('管理员广告类型API权限控制', () => {
  147. it('超级管理员(ID=1)应该能访问管理员广告类型列表API', async () => {
  148. const response = await adminTypeClient.$get(undefined, {
  149. headers: {
  150. 'Authorization': `Bearer ${superAdminToken}`,
  151. 'X-Tenant-ID': '1',
  152. 'X-User-ID': '1'
  153. }
  154. });
  155. expect([200, 404]).toContain(response.status);
  156. if (response.status === 200) {
  157. const data = await response.json();
  158. // API返回格式: { code, message, data: { list, total } }
  159. expect(data).toHaveProperty('data');
  160. expect(data.data).toHaveProperty('list');
  161. expect(Array.isArray(data.data.list)).toBeTruthy();
  162. }
  163. });
  164. it('普通管理员(ID>1)不应该能访问管理员广告类型列表API', async () => {
  165. const response = await adminTypeClient.$get(undefined, {
  166. headers: {
  167. 'Authorization': `Bearer ${regularUserToken}`,
  168. 'X-Tenant-ID': '1',
  169. 'X-User-ID': '2'
  170. }
  171. });
  172. expect([401, 403]).toContain(response.status);
  173. });
  174. it('未认证用户不应该能访问管理员广告类型列表API', async () => {
  175. const response = await adminTypeClient.$get();
  176. expect(response.status).toBe(401);
  177. });
  178. });
  179. describe('用户端广告API访问控制', () => {
  180. it('认证用户应该能访问用户端广告列表API', async () => {
  181. const response = await userClient.$get(undefined, {
  182. headers: {
  183. 'Authorization': `Bearer ${tenantUserToken}`,
  184. 'X-Tenant-ID': '2',
  185. 'X-User-ID': '3'
  186. }
  187. });
  188. // 用户端API应该返回数据 (200) 或空列表
  189. expect([200, 404]).toContain(response.status);
  190. if (response.status === 200) {
  191. const data = await response.json();
  192. expect(data).toHaveProperty("data");
  193. expect(data.data).toHaveProperty("list");
  194. expect(Array.isArray(data.data.list)).toBeTruthy();
  195. }
  196. });
  197. it('未认证用户应该能访问用户端广告列表API(公开访问)', async () => {
  198. const response = await userClient.$get();
  199. // 用户端广告API可能是公开的,允许未认证访问
  200. expect([200, 401, 404]).toContain(response.status);
  201. });
  202. it('认证用户应该能访问用户端广告类型列表API', async () => {
  203. const response = await userTypeClient.$get(undefined, {
  204. headers: {
  205. 'Authorization': `Bearer ${tenantUserToken}`,
  206. 'X-Tenant-ID': '2',
  207. 'X-User-ID': '3'
  208. }
  209. });
  210. expect([200, 404]).toContain(response.status);
  211. if (response.status === 200) {
  212. const data = await response.json();
  213. // API返回格式: { code, message, data: { list, total } }
  214. expect(data).toHaveProperty('data');
  215. expect(data.data).toHaveProperty('list');
  216. expect(Array.isArray(data.data.list)).toBeTruthy();
  217. }
  218. });
  219. });
  220. describe('统一广告数据隔离验证', () => {
  221. it('用户端API应该返回统一的广告数据(无tenant_id过滤)', async () => {
  222. const dataSource = await IntegrationTestDatabase.getDataSource();
  223. if (!dataSource) throw new Error('Database not initialized');
  224. // 先创建广告类型
  225. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  226. const adType = await adTypeRepository.save({
  227. name: 'Test Type',
  228. code: 'test_type_verify',
  229. sort: 0,
  230. status: 1
  231. });
  232. // 创建测试广告数据
  233. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  234. await adRepository.save({
  235. title: 'Test Ad',
  236. typeId: adType.id,
  237. code: 'test_ad_verify',
  238. url: 'http://example.com',
  239. status: 1
  240. });
  241. const response = await userClient.$get(undefined, {
  242. headers: {
  243. 'Authorization': `Bearer ${tenantUserToken}`,
  244. 'X-Tenant-ID': '2',
  245. 'X-User-ID': '3'
  246. }
  247. });
  248. if (response.status === 200) {
  249. const data = await response.json();
  250. // API返回格式: { code, message, data: { list, total } }
  251. expect(data).toHaveProperty('data');
  252. expect(data.data).toHaveProperty('list');
  253. expect(Array.isArray(data.data.list)).toBeTruthy();
  254. // 验证返回的是统一广告数据,不是按租户隔离的
  255. if (data.data.list.length > 0) {
  256. const ad = data.data.list[0];
  257. expect(ad).not.toHaveProperty('tenantId'); // 统一广告不应该有tenantId字段
  258. }
  259. }
  260. });
  261. });
  262. describe('API路径兼容性验证', () => {
  263. it('用户端广告API路径应该保持兼容', async () => {
  264. const response = await userClient.$get();
  265. // API端点应该可访问
  266. expect([200, 401, 404]).toContain(response.status);
  267. });
  268. it('用户端广告类型API路径应该保持兼容', async () => {
  269. const response = await userTypeClient.$get();
  270. // API端点应该可访问
  271. expect([200, 401, 404]).toContain(response.status);
  272. });
  273. });
  274. describe('管理员操作权限验证', () => {
  275. it('超级管理员应该能创建统一广告', async () => {
  276. const dataSource = await IntegrationTestDatabase.getDataSource();
  277. if (!dataSource) throw new Error('Database not initialized');
  278. // 先创建广告类型
  279. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  280. const adType = await adTypeRepository.save({
  281. name: 'Test Type',
  282. code: 'test_type',
  283. sort: 0,
  284. status: 1
  285. });
  286. const newAd = {
  287. title: 'New Unified Ad',
  288. typeId: adType.id, // 使用创建的广告类型ID
  289. code: 'test_ad', // 必填
  290. url: 'http://example.com/new',
  291. status: 1
  292. };
  293. const response = await adminClient.$post({
  294. json: newAd
  295. }, {
  296. headers: {
  297. 'Authorization': `Bearer ${superAdminToken}`,
  298. 'X-Tenant-ID': '1',
  299. 'X-User-ID': '1'
  300. }
  301. });
  302. // 超级管理员应该能创建
  303. expect([200, 201]).toContain(response.status);
  304. });
  305. it('普通管理员不应该能创建统一广告', async () => {
  306. const dataSource = await IntegrationTestDatabase.getDataSource();
  307. if (!dataSource) throw new Error('Database not initialized');
  308. // 先创建广告类型
  309. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  310. const adType = await adTypeRepository.save({
  311. name: 'Test Type 2',
  312. code: 'test_type_2',
  313. sort: 0,
  314. status: 1
  315. });
  316. const newAd = {
  317. title: 'New Unified Ad',
  318. typeId: adType.id,
  319. code: 'test_ad_2',
  320. url: 'http://example.com/new',
  321. status: 1
  322. };
  323. const response = await adminClient.$post({
  324. json: newAd
  325. }, {
  326. headers: {
  327. 'Authorization': `Bearer ${regularUserToken}`,
  328. 'X-Tenant-ID': '1',
  329. 'X-User-ID': '2'
  330. }
  331. });
  332. // 普通管理员应该被拒绝
  333. expect([401, 403]).toContain(response.status);
  334. });
  335. it('超级管理员应该能更新统一广告', async () => {
  336. const dataSource = await IntegrationTestDatabase.getDataSource();
  337. if (!dataSource) throw new Error('Database not initialized');
  338. // 先创建广告类型
  339. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  340. const adType = await adTypeRepository.save({
  341. name: 'Test Type Update',
  342. code: 'test_type_update',
  343. sort: 0,
  344. status: 1
  345. });
  346. // 创建测试广告
  347. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  348. const testAd = await adRepository.save({
  349. title: 'Test Ad',
  350. typeId: adType.id,
  351. code: 'test_ad_update',
  352. url: 'http://example.com',
  353. status: 1
  354. });
  355. const updateData = {
  356. title: 'Updated Test Ad'
  357. };
  358. const response = await adminClient[':id'].$put({
  359. param: { id: testAd.id },
  360. json: updateData
  361. }, {
  362. headers: {
  363. 'Authorization': `Bearer ${superAdminToken}`,
  364. 'X-Tenant-ID': '1',
  365. 'X-User-ID': '1'
  366. }
  367. });
  368. // 超级管理员应该能更新
  369. expect([200, 404]).toContain(response.status);
  370. });
  371. it('超级管理员应该能删除统一广告', async () => {
  372. const dataSource = await IntegrationTestDatabase.getDataSource();
  373. if (!dataSource) throw new Error('Database not initialized');
  374. // 先创建广告类型
  375. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  376. const adType = await adTypeRepository.save({
  377. name: 'Test Type Delete',
  378. code: 'test_type_delete',
  379. sort: 0,
  380. status: 1
  381. });
  382. // 创建测试广告
  383. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  384. const testAd = await adRepository.save({
  385. title: 'Test Ad',
  386. typeId: adType.id,
  387. code: 'test_ad_delete',
  388. url: 'http://example.com',
  389. status: 1
  390. });
  391. const response = await adminClient[':id'].$delete({
  392. param: { id: testAd.id }
  393. }, {
  394. headers: {
  395. 'Authorization': `Bearer ${superAdminToken}`,
  396. 'X-Tenant-ID': '1',
  397. 'X-User-ID': '1'
  398. }
  399. });
  400. // 超级管理员应该能删除
  401. expect([200, 204, 404]).toContain(response.status);
  402. });
  403. });
  404. });