areas.integration.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. import { describe, it, expect, beforeEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import {
  4. IntegrationTestDatabase,
  5. setupIntegrationDatabaseHooksWithEntities,
  6. IntegrationTestAssertions
  7. } from '@d8d/shared-test-util';
  8. import { areasRoutesMt } from '../../src/api/areas/index.mt';
  9. import { AreaEntityMt, AreaLevel } from '../../src/modules/areas/area.entity.mt';
  10. import { DisabledStatus } from '@d8d/shared-types';
  11. import { TestDataFactory } from '../utils/test-data-factory';
  12. import { TestQueryFactory } from '../utils/test-query-factory';
  13. import { AuthService } from '@d8d/auth-module-mt';
  14. import { UserServiceMt } from '@d8d/user-module-mt';
  15. import { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
  16. import { FileMt } from '@d8d/file-module-mt';
  17. // 定义响应类型
  18. interface SuccessResponse {
  19. success: boolean;
  20. data: {
  21. provinces: Array<{
  22. id: number;
  23. name: string;
  24. code: string;
  25. level: number;
  26. parentId: number | null;
  27. }>;
  28. pagination: {
  29. page: number;
  30. pageSize: number;
  31. total: number;
  32. totalPages: number;
  33. };
  34. };
  35. message: string;
  36. }
  37. interface ErrorResponse {
  38. code: number;
  39. message: string;
  40. errors?: Array<{
  41. path: string[];
  42. message: string;
  43. }>;
  44. }
  45. // 设置集成测试钩子
  46. setupIntegrationDatabaseHooksWithEntities([AreaEntityMt, UserEntityMt, FileMt, RoleMt])
  47. describe('区域API集成测试', () => {
  48. let client: ReturnType<typeof testClient<typeof areasRoutesMt>>;
  49. let authService: AuthService;
  50. let userService: UserServiceMt;
  51. let testToken: string;
  52. let testUser: any;
  53. let testAreas: AreaEntityMt[];
  54. beforeEach(async () => {
  55. // 创建测试客户端
  56. client = testClient(areasRoutesMt);
  57. // 获取数据源
  58. const dataSource = await IntegrationTestDatabase.getDataSource();
  59. if (!dataSource) throw new Error('Database not initialized');
  60. // 初始化服务
  61. userService = new UserServiceMt(dataSource);
  62. authService = new AuthService(userService);
  63. // 创建测试用户并生成token
  64. testUser = await TestDataFactory.createTestUser(dataSource, {
  65. username: 'testuser_areas',
  66. password: 'TestPassword123!',
  67. email: 'testuser_areas@example.com'
  68. });
  69. // 生成测试用户的token
  70. testToken = authService.generateToken(testUser);
  71. // 创建测试数据
  72. // 创建启用状态的省份(租户1)
  73. const province1 = await TestDataFactory.createTestArea(dataSource, {
  74. name: '北京市',
  75. level: AreaLevel.PROVINCE,
  76. isDisabled: DisabledStatus.ENABLED,
  77. tenantId: 1
  78. });
  79. const province2 = await TestDataFactory.createTestArea(dataSource, {
  80. name: '上海市',
  81. level: AreaLevel.PROVINCE,
  82. isDisabled: DisabledStatus.ENABLED,
  83. tenantId: 1
  84. });
  85. const province3 = await TestDataFactory.createTestArea(dataSource, {
  86. name: '广东省',
  87. level: AreaLevel.PROVINCE,
  88. isDisabled: DisabledStatus.ENABLED,
  89. tenantId: 1
  90. });
  91. // 创建启用状态的城市
  92. const city11 = await TestDataFactory.createTestArea(dataSource, {
  93. name: '北京市',
  94. level: AreaLevel.CITY,
  95. parentId: province1.id,
  96. isDisabled: DisabledStatus.ENABLED
  97. });
  98. const city12 = await TestDataFactory.createTestArea(dataSource, {
  99. name: '朝阳区',
  100. level: AreaLevel.CITY,
  101. parentId: province1.id,
  102. isDisabled: DisabledStatus.ENABLED
  103. });
  104. const city13 = await TestDataFactory.createTestArea(dataSource, {
  105. name: '海淀区',
  106. level: AreaLevel.CITY,
  107. parentId: province1.id,
  108. isDisabled: DisabledStatus.ENABLED
  109. });
  110. const city21 = await TestDataFactory.createTestArea(dataSource, {
  111. name: '上海市',
  112. level: AreaLevel.CITY,
  113. parentId: province2.id,
  114. isDisabled: DisabledStatus.ENABLED
  115. });
  116. const city22 = await TestDataFactory.createTestArea(dataSource, {
  117. name: '浦东新区',
  118. level: AreaLevel.CITY,
  119. parentId: province2.id,
  120. isDisabled: DisabledStatus.ENABLED
  121. });
  122. // 创建启用状态的区县
  123. const district101 = await TestDataFactory.createTestArea(dataSource, {
  124. name: '朝阳区',
  125. level: AreaLevel.DISTRICT,
  126. parentId: city12.id,
  127. isDisabled: DisabledStatus.ENABLED
  128. });
  129. const district102 = await TestDataFactory.createTestArea(dataSource, {
  130. name: '海淀区',
  131. level: AreaLevel.DISTRICT,
  132. parentId: city13.id,
  133. isDisabled: DisabledStatus.ENABLED
  134. });
  135. const district103 = await TestDataFactory.createTestArea(dataSource, {
  136. name: '西城区',
  137. level: AreaLevel.DISTRICT,
  138. parentId: city12.id,
  139. isDisabled: DisabledStatus.ENABLED
  140. });
  141. const district201 = await TestDataFactory.createTestArea(dataSource, {
  142. name: '浦东新区',
  143. level: AreaLevel.DISTRICT,
  144. parentId: city22.id,
  145. isDisabled: DisabledStatus.ENABLED
  146. });
  147. // 创建禁用状态的区域用于测试过滤
  148. const disabledProvince = await TestDataFactory.createTestArea(dataSource, {
  149. name: '禁用省份',
  150. level: AreaLevel.PROVINCE,
  151. isDisabled: DisabledStatus.DISABLED
  152. });
  153. const disabledCity = await TestDataFactory.createTestArea(dataSource, {
  154. name: '禁用城市',
  155. level: AreaLevel.CITY,
  156. parentId: province3.id,
  157. isDisabled: DisabledStatus.DISABLED
  158. });
  159. const disabledDistrict = await TestDataFactory.createTestArea(dataSource, {
  160. name: '禁用区县',
  161. level: AreaLevel.DISTRICT,
  162. parentId: city12.id,
  163. isDisabled: DisabledStatus.DISABLED
  164. });
  165. testAreas = [
  166. province1, province2, province3,
  167. city11, city12, city13, city21, city22,
  168. district101, district102, district103, district201,
  169. disabledProvince, disabledCity, disabledDistrict
  170. ];
  171. });
  172. describe('GET /areas/provinces', () => {
  173. it('应该成功获取启用状态的省份列表', async () => {
  174. const response = await client.provinces.$get({
  175. query: TestQueryFactory.createProvincesQuery()
  176. }, {
  177. headers: {
  178. 'Authorization': `Bearer ${testToken}`
  179. }
  180. });
  181. IntegrationTestAssertions.expectStatus(response, 200);
  182. if (response.status === 200) {
  183. const data = await response.json();
  184. // 验证响应数据格式
  185. expect(data).toHaveProperty('success', true);
  186. expect(data).toHaveProperty('data');
  187. expect(data.data).toHaveProperty('provinces');
  188. expect(data.data).toHaveProperty('pagination');
  189. // 验证只返回启用状态的省份
  190. const provinces = data.data.provinces;
  191. expect(provinces).toHaveLength(3); // 只返回3个启用状态的省份
  192. // 验证不包含禁用状态的省份
  193. const disabledProvince = provinces.find((p: any) => p.isDisabled === DisabledStatus.DISABLED);
  194. expect(disabledProvince).toBeUndefined();
  195. // 验证分页信息
  196. expect(data.data.pagination).toEqual({
  197. page: 1,
  198. pageSize: 50,
  199. total: 3,
  200. totalPages: 1
  201. });
  202. }
  203. });
  204. it('应该正确处理分页参数', async () => {
  205. const response = await client.provinces.$get({
  206. query: TestQueryFactory.createPaginationQuery(1, 2)
  207. }, {
  208. headers: {
  209. 'Authorization': `Bearer ${testToken}`
  210. }
  211. });
  212. IntegrationTestAssertions.expectStatus(response, 200);
  213. if (response.status === 200) {
  214. const data = await response.json();
  215. // 验证分页结果
  216. expect(data.data.provinces).toHaveLength(2);
  217. expect(data.data.pagination).toEqual({
  218. page: 1,
  219. pageSize: 2,
  220. total: 3,
  221. totalPages: 2
  222. });
  223. }
  224. });
  225. });
  226. describe('GET /areas/cities', () => {
  227. it('应该成功获取指定省份下启用状态的城市列表', async () => {
  228. const response = await client.cities.$get({
  229. query: TestQueryFactory.createCitiesQuery(testAreas[0].id)
  230. }, {
  231. headers: {
  232. 'Authorization': `Bearer ${testToken}`
  233. }
  234. });
  235. IntegrationTestAssertions.expectStatus(response, 200);
  236. if (response.status === 200) {
  237. const data = await response.json();
  238. // 验证响应数据格式
  239. expect(data).toHaveProperty('success', true);
  240. expect(data).toHaveProperty('data');
  241. expect(data.data).toHaveProperty('cities');
  242. // 验证只返回启用状态的城市
  243. const cities = data.data.cities;
  244. expect(cities).toHaveLength(3); // 北京市下有3个启用状态的城市
  245. // 验证城市数据正确
  246. const cityNames = cities.map((c: any) => c.name);
  247. expect(cityNames).toContain('北京市');
  248. expect(cityNames).toContain('朝阳区');
  249. expect(cityNames).toContain('海淀区');
  250. // 验证不包含禁用状态的城市
  251. const disabledCity = cities.find((c: any) => c.name === '禁用城市');
  252. expect(disabledCity).toBeUndefined();
  253. }
  254. });
  255. it('应该处理不存在的省份ID', async () => {
  256. const response = await client.cities.$get({
  257. query: TestQueryFactory.createCitiesQuery(999)
  258. }, {
  259. headers: {
  260. 'Authorization': `Bearer ${testToken}`
  261. }
  262. });
  263. IntegrationTestAssertions.expectStatus(response, 200);
  264. if (response.status === 200) {
  265. const data = await response.json();
  266. // 不存在的省份应该返回空数组
  267. expect(data.data.cities).toHaveLength(0);
  268. }
  269. });
  270. it('应该验证省份ID参数', async () => {
  271. const response = await client.cities.$get({
  272. query: TestQueryFactory.createCitiesQuery(0)
  273. }, {
  274. headers: {
  275. 'Authorization': `Bearer ${testToken}`
  276. }
  277. });
  278. // 参数验证应该返回400错误
  279. IntegrationTestAssertions.expectStatus(response, 400);
  280. });
  281. });
  282. describe('GET /areas/districts', () => {
  283. it('应该成功获取指定城市下启用状态的区县列表', async () => {
  284. // 找到朝阳区城市对象
  285. const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
  286. expect(chaoyangCity).toBeDefined();
  287. const response = await client.districts.$get({
  288. query: TestQueryFactory.createDistrictsQuery(chaoyangCity!.id)
  289. }, {
  290. headers: {
  291. 'Authorization': `Bearer ${testToken}`
  292. }
  293. });
  294. IntegrationTestAssertions.expectStatus(response, 200);
  295. if (response.status === 200) {
  296. const data = await response.json();
  297. // 验证响应数据格式
  298. expect(data).toHaveProperty('success', true);
  299. expect(data).toHaveProperty('data');
  300. expect(data.data).toHaveProperty('districts');
  301. // 验证只返回启用状态的区县
  302. const districts = data.data.districts;
  303. expect(districts).toHaveLength(2); // 朝阳区下有2个启用状态的区县
  304. // 验证区县数据正确
  305. const districtNames = districts.map((d: any) => d.name);
  306. expect(districtNames).toContain('朝阳区');
  307. expect(districtNames).toContain('西城区');
  308. // 验证不包含禁用状态的区县
  309. const disabledDistrict = districts.find((d: any) => d.name === '禁用区县');
  310. expect(disabledDistrict).toBeUndefined();
  311. }
  312. });
  313. it('应该处理不存在的城市ID', async () => {
  314. const response = await client.districts.$get({
  315. query: TestQueryFactory.createDistrictsQuery(999)
  316. }, {
  317. headers: {
  318. 'Authorization': `Bearer ${testToken}`
  319. }
  320. });
  321. IntegrationTestAssertions.expectStatus(response, 200);
  322. if (response.status === 200) {
  323. const data = await response.json();
  324. // 不存在的城市应该返回空数组
  325. expect(data.data.districts).toHaveLength(0);
  326. }
  327. });
  328. it('应该验证城市ID参数', async () => {
  329. const response = await client.districts.$get({
  330. query: TestQueryFactory.createDistrictsQuery(0)
  331. }, {
  332. headers: {
  333. 'Authorization': `Bearer ${testToken}`
  334. }
  335. });
  336. // 参数验证应该返回400错误
  337. IntegrationTestAssertions.expectStatus(response, 400);
  338. });
  339. });
  340. describe('过滤禁用状态验证', () => {
  341. it('应该确保所有API只返回启用状态的区域', async () => {
  342. // 测试省份API
  343. const provincesResponse = await client.provinces.$get({
  344. query: TestQueryFactory.createProvincesQuery()
  345. }, {
  346. headers: {
  347. 'Authorization': `Bearer ${testToken}`
  348. }
  349. });
  350. IntegrationTestAssertions.expectStatus(provincesResponse, 200);
  351. const provincesData = await provincesResponse.json();
  352. // 验证省份不包含禁用状态
  353. if ('data' in provincesData) {
  354. const provinces = provincesData.data.provinces;
  355. const hasDisabledProvince = provinces.some((p: any) => p.isDisabled === DisabledStatus.DISABLED);
  356. expect(hasDisabledProvince).toBe(false);
  357. }
  358. // 测试城市API
  359. const citiesResponse = await client.cities.$get({
  360. query: TestQueryFactory.createCitiesQuery(testAreas[0].id)
  361. }, {
  362. headers: {
  363. 'Authorization': `Bearer ${testToken}`
  364. }
  365. });
  366. IntegrationTestAssertions.expectStatus(citiesResponse, 200);
  367. const citiesData = await citiesResponse.json();
  368. // 验证城市不包含禁用状态
  369. if ('data' in citiesData) {
  370. const cities = citiesData.data.cities;
  371. const hasDisabledCity = cities.some((c: any) => c.isDisabled === DisabledStatus.DISABLED);
  372. expect(hasDisabledCity).toBe(false);
  373. }
  374. // 测试区县API
  375. const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
  376. const districtsResponse = await client.districts.$get({
  377. query: TestQueryFactory.createDistrictsQuery(chaoyangCity!.id)
  378. }, {
  379. headers: {
  380. 'Authorization': `Bearer ${testToken}`
  381. }
  382. });
  383. IntegrationTestAssertions.expectStatus(districtsResponse, 200);
  384. const districtsData = await districtsResponse.json();
  385. // 验证区县不包含禁用状态
  386. if ('data' in districtsData) {
  387. const districts = districtsData.data.districts;
  388. const hasDisabledDistrict = districts.some((d: any) => d.isDisabled === DisabledStatus.DISABLED);
  389. expect(hasDisabledDistrict).toBe(false);
  390. }
  391. });
  392. });
  393. describe('租户数据隔离测试', () => {
  394. let tenant2Token: string;
  395. let tenant2User: any;
  396. beforeEach(async () => {
  397. // 为租户2创建测试数据
  398. const dataSource = await IntegrationTestDatabase.getDataSource();
  399. // 创建租户2的用户
  400. tenant2User = await TestDataFactory.createTestUser(dataSource, {
  401. username: 'testuser_tenant2',
  402. password: 'TestPassword123!',
  403. email: 'testuser_tenant2@example.com',
  404. tenantId: 2
  405. });
  406. // 生成租户2用户的token
  407. tenant2Token = authService.generateToken(tenant2User);
  408. // 租户2的省份
  409. await TestDataFactory.createTestArea(dataSource, {
  410. name: '租户2-北京市',
  411. level: AreaLevel.PROVINCE,
  412. isDisabled: DisabledStatus.ENABLED,
  413. tenantId: 2
  414. });
  415. // 租户2的城市
  416. await TestDataFactory.createTestArea(dataSource, {
  417. name: '租户2-上海市',
  418. level: AreaLevel.PROVINCE,
  419. isDisabled: DisabledStatus.ENABLED,
  420. tenantId: 2
  421. });
  422. });
  423. it('应该只返回指定租户的数据', async () => {
  424. // 测试租户1的数据
  425. const response1 = await client.provinces.$get({
  426. query: TestQueryFactory.createProvincesQuery()
  427. }, {
  428. headers: {
  429. 'Authorization': `Bearer ${testToken}`
  430. }
  431. });
  432. IntegrationTestAssertions.expectStatus(response1, 200);
  433. const data1 = await response1.json();
  434. // 验证租户1只看到租户1的数据
  435. const successData1 = data1 as SuccessResponse;
  436. if (successData1.success) {
  437. const tenant1Provinces = successData1.data.provinces;
  438. expect(tenant1Provinces).toHaveLength(3); // 租户1有3个省份
  439. const hasTenant2Data = tenant1Provinces.some((p: any) => p.name.includes('租户2'));
  440. expect(hasTenant2Data).toBe(false);
  441. } else {
  442. throw new Error('租户1数据获取失败');
  443. }
  444. // 测试租户2的数据
  445. const response2 = await client.provinces.$get({
  446. query: { page: 1, pageSize: 50 }
  447. }, {
  448. headers: {
  449. 'Authorization': `Bearer ${tenant2Token}`
  450. }
  451. });
  452. IntegrationTestAssertions.expectStatus(response2, 200);
  453. const data2 = await response2.json();
  454. // 验证租户2只看到租户2的数据
  455. const successData2 = data2 as SuccessResponse;
  456. if (successData2.success) {
  457. const tenant2Provinces = successData2.data.provinces;
  458. expect(tenant2Provinces).toHaveLength(2); // 租户2有2个省份
  459. const hasTenant1Data = tenant2Provinces.some((p: any) => p.name.includes('北京市') && !p.name.includes('租户2'));
  460. expect(hasTenant1Data).toBe(false);
  461. } else {
  462. throw new Error('租户2数据获取失败');
  463. }
  464. });
  465. it('不同租户的数据应该完全隔离', async () => {
  466. // 租户1查询省份
  467. const response1 = await client.provinces.$get({
  468. query: TestQueryFactory.createProvincesQuery()
  469. }, {
  470. headers: {
  471. 'Authorization': `Bearer ${testToken}`
  472. }
  473. });
  474. IntegrationTestAssertions.expectStatus(response1, 200);
  475. const data1 = await response1.json();
  476. // 租户2查询省份
  477. const response2 = await client.provinces.$get({
  478. query: { page: 1, pageSize: 50 }
  479. }, {
  480. headers: {
  481. 'Authorization': `Bearer ${tenant2Token}`
  482. }
  483. });
  484. IntegrationTestAssertions.expectStatus(response2, 200);
  485. const data2 = await response2.json();
  486. // 验证两个租户的数据完全不同
  487. const successData1 = data1 as SuccessResponse;
  488. const successData2 = data2 as SuccessResponse;
  489. if (successData1.success && successData2.success) {
  490. const tenant1Names = successData1.data.provinces.map((p: any) => p.name);
  491. const tenant2Names = successData2.data.provinces.map((p: any) => p.name);
  492. expect(tenant1Names).not.toEqual(tenant2Names);
  493. expect(tenant1Names).toContain('北京市');
  494. expect(tenant1Names).toContain('上海市');
  495. expect(tenant1Names).toContain('广东省');
  496. expect(tenant2Names).toContain('租户2-北京市');
  497. expect(tenant2Names).toContain('租户2-上海市');
  498. } else {
  499. throw new Error('租户数据获取失败');
  500. }
  501. });
  502. it('应该验证认证令牌', async () => {
  503. // 测试缺少认证令牌
  504. const response = await client.provinces.$get({
  505. query: { page: 1, pageSize: 50 }
  506. });
  507. // 应该返回401错误,因为缺少认证
  508. IntegrationTestAssertions.expectStatus(response, 401);
  509. });
  510. it('应该处理不存在的租户ID', async () => {
  511. // 创建不存在的租户用户
  512. const dataSource = await IntegrationTestDatabase.getDataSource();
  513. const nonExistentTenantUser = await TestDataFactory.createTestUser(dataSource, {
  514. username: 'testuser_nonexistent',
  515. password: 'TestPassword123!',
  516. email: 'testuser_nonexistent@example.com',
  517. tenantId: 999 // 不存在的租户ID
  518. });
  519. const nonExistentTenantToken = authService.generateToken(nonExistentTenantUser);
  520. const response = await client.provinces.$get({
  521. query: { page: 1, pageSize: 50 }
  522. }, {
  523. headers: {
  524. 'Authorization': `Bearer ${nonExistentTenantToken}`
  525. }
  526. });
  527. IntegrationTestAssertions.expectStatus(response, 200);
  528. const data = await response.json();
  529. // 不存在的租户应该返回空数组
  530. const successData = data as SuccessResponse;
  531. if (successData.success) {
  532. expect(successData.data.provinces).toHaveLength(0);
  533. } else {
  534. throw new Error('不存在的租户数据获取失败');
  535. }
  536. });
  537. });
  538. });