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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  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返回格式: { data: [...], pagination: { total, current, pageSize } }
  113. expect(data).toHaveProperty('data');
  114. expect(Array.isArray(data.data)).toBe(true);
  115. expect(data).toHaveProperty('pagination');
  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返回格式: { data: [...], pagination: { total, current, pageSize } }
  159. expect(data).toHaveProperty('data');
  160. expect(Array.isArray(data.data)).toBe(true);
  161. expect(data).toHaveProperty('pagination');
  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. // API返回格式: { data: [...], pagination: { total, current, pageSize } }
  193. expect(data).toHaveProperty('data');
  194. expect(Array.isArray(data.data)).toBe(true);
  195. expect(data).toHaveProperty('pagination');
  196. }
  197. });
  198. it('未认证用户应该能访问用户端广告列表API(公开访问)', async () => {
  199. const response = await userClient.$get();
  200. // 用户端广告API可能是公开的,允许未认证访问
  201. expect([200, 401, 404]).toContain(response.status);
  202. });
  203. it('认证用户应该能访问用户端广告类型列表API', async () => {
  204. const response = await userTypeClient.$get(undefined, {
  205. headers: {
  206. 'Authorization': `Bearer ${tenantUserToken}`,
  207. 'X-Tenant-ID': '2',
  208. 'X-User-ID': '3'
  209. }
  210. });
  211. expect([200, 404]).toContain(response.status);
  212. if (response.status === 200) {
  213. const data = await response.json();
  214. // API返回格式: { data: [...], pagination: { total, current, pageSize } }
  215. expect(data).toHaveProperty('data');
  216. expect(Array.isArray(data.data)).toBe(true);
  217. expect(data).toHaveProperty('pagination');
  218. }
  219. });
  220. });
  221. describe('统一广告数据隔离验证', () => {
  222. it('用户端API应该返回统一的广告数据(无tenant_id过滤)', async () => {
  223. const dataSource = await IntegrationTestDatabase.getDataSource();
  224. if (!dataSource) throw new Error('Database not initialized');
  225. // 先创建广告类型
  226. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  227. const adType = await adTypeRepository.save({
  228. name: 'Test Type',
  229. code: 'test_type_verify',
  230. sort: 0,
  231. status: 1
  232. });
  233. // 创建测试广告数据
  234. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  235. await adRepository.save({
  236. title: 'Test Ad',
  237. typeId: adType.id,
  238. code: 'test_ad_verify',
  239. url: 'http://example.com',
  240. status: 1
  241. });
  242. const response = await userClient.$get(undefined, {
  243. headers: {
  244. 'Authorization': `Bearer ${tenantUserToken}`,
  245. 'X-Tenant-ID': '2',
  246. 'X-User-ID': '3'
  247. }
  248. });
  249. if (response.status === 200) {
  250. const data = await response.json();
  251. // API返回格式: { data: [...], pagination: { total, current, pageSize } }
  252. expect(data).toHaveProperty('data');
  253. expect(Array.isArray(data.data)).toBe(true);
  254. expect(data).toHaveProperty('pagination');
  255. // 验证返回的是统一广告数据,不是按租户隔离的
  256. if (data.data.length > 0) {
  257. const ad = data.data[0];
  258. expect(ad).not.toHaveProperty('tenantId'); // 统一广告不应该有tenantId字段
  259. }
  260. }
  261. });
  262. });
  263. describe('API路径兼容性验证', () => {
  264. it('用户端广告API路径应该保持兼容', async () => {
  265. const response = await userClient.$get();
  266. // API端点应该可访问
  267. expect([200, 401, 404]).toContain(response.status);
  268. });
  269. it('用户端广告类型API路径应该保持兼容', async () => {
  270. const response = await userTypeClient.$get();
  271. // API端点应该可访问
  272. expect([200, 401, 404]).toContain(response.status);
  273. });
  274. });
  275. describe('管理员操作权限验证', () => {
  276. it('超级管理员应该能创建统一广告', async () => {
  277. const dataSource = await IntegrationTestDatabase.getDataSource();
  278. if (!dataSource) throw new Error('Database not initialized');
  279. // 先创建广告类型
  280. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  281. const adType = await adTypeRepository.save({
  282. name: 'Test Type',
  283. code: 'test_type',
  284. sort: 0,
  285. status: 1
  286. });
  287. const newAd = {
  288. title: 'New Unified Ad',
  289. typeId: adType.id, // 使用创建的广告类型ID
  290. code: 'test_ad', // 必填
  291. url: 'http://example.com/new',
  292. status: 1
  293. };
  294. const response = await adminClient.$post({
  295. json: newAd
  296. }, {
  297. headers: {
  298. 'Authorization': `Bearer ${superAdminToken}`,
  299. 'X-Tenant-ID': '1',
  300. 'X-User-ID': '1'
  301. }
  302. });
  303. // 超级管理员应该能创建
  304. expect([200, 201]).toContain(response.status);
  305. });
  306. it('普通管理员不应该能创建统一广告', async () => {
  307. const dataSource = await IntegrationTestDatabase.getDataSource();
  308. if (!dataSource) throw new Error('Database not initialized');
  309. // 先创建广告类型
  310. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  311. const adType = await adTypeRepository.save({
  312. name: 'Test Type 2',
  313. code: 'test_type_2',
  314. sort: 0,
  315. status: 1
  316. });
  317. const newAd = {
  318. title: 'New Unified Ad',
  319. typeId: adType.id,
  320. code: 'test_ad_2',
  321. url: 'http://example.com/new',
  322. status: 1
  323. };
  324. const response = await adminClient.$post({
  325. json: newAd
  326. }, {
  327. headers: {
  328. 'Authorization': `Bearer ${regularUserToken}`,
  329. 'X-Tenant-ID': '1',
  330. 'X-User-ID': '2'
  331. }
  332. });
  333. // 普通管理员应该被拒绝
  334. expect([401, 403]).toContain(response.status);
  335. });
  336. it('超级管理员应该能更新统一广告', async () => {
  337. const dataSource = await IntegrationTestDatabase.getDataSource();
  338. if (!dataSource) throw new Error('Database not initialized');
  339. // 先创建广告类型
  340. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  341. const adType = await adTypeRepository.save({
  342. name: 'Test Type Update',
  343. code: 'test_type_update',
  344. sort: 0,
  345. status: 1
  346. });
  347. // 创建测试广告
  348. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  349. const testAd = await adRepository.save({
  350. title: 'Test Ad',
  351. typeId: adType.id,
  352. code: 'test_ad_update',
  353. url: 'http://example.com',
  354. status: 1
  355. });
  356. const updateData = {
  357. title: 'Updated Test Ad'
  358. };
  359. const response = await adminClient[':id'].$put({
  360. param: { id: testAd.id },
  361. json: updateData
  362. }, {
  363. headers: {
  364. 'Authorization': `Bearer ${superAdminToken}`,
  365. 'X-Tenant-ID': '1',
  366. 'X-User-ID': '1'
  367. }
  368. });
  369. // 超级管理员应该能更新
  370. expect([200, 404]).toContain(response.status);
  371. });
  372. it('超级管理员应该能删除统一广告', async () => {
  373. const dataSource = await IntegrationTestDatabase.getDataSource();
  374. if (!dataSource) throw new Error('Database not initialized');
  375. // 先创建广告类型
  376. const adTypeRepository = dataSource.getRepository(UnifiedAdvertisementType);
  377. const adType = await adTypeRepository.save({
  378. name: 'Test Type Delete',
  379. code: 'test_type_delete',
  380. sort: 0,
  381. status: 1
  382. });
  383. // 创建测试广告
  384. const adRepository = dataSource.getRepository(UnifiedAdvertisement);
  385. const testAd = await adRepository.save({
  386. title: 'Test Ad',
  387. typeId: adType.id,
  388. code: 'test_ad_delete',
  389. url: 'http://example.com',
  390. status: 1
  391. });
  392. const response = await adminClient[':id'].$delete({
  393. param: { id: testAd.id }
  394. }, {
  395. headers: {
  396. 'Authorization': `Bearer ${superAdminToken}`,
  397. 'X-Tenant-ID': '1',
  398. 'X-User-ID': '1'
  399. }
  400. });
  401. // 超级管理员应该能删除
  402. expect([200, 204, 404]).toContain(response.status);
  403. });
  404. });
  405. });