Forráskód Böngészése

♻️ refactor(areas): 优化区域API数据源依赖注入

- 引入共享AppDataSource替代请求上下文数据源
- 统一所有路由中AreaService的实例化方式
- 导出areaRoutes变量以便测试使用

✅ test(areas): 添加区域API集成测试

- 创建区域API完整集成测试套件
- 测试省份、城市、区县三级区域查询功能
- 验证分页、参数校验和禁用状态过滤逻辑
- 覆盖正常流程和边界情况测试场景

🔧 chore(areas): 添加测试数据工厂工具类

- 实现TestDataFactory创建标准化测试区域数据
- 支持不同级别(省/市/区县)区域创建
- 自动处理区域间父子关系依赖
yourname 4 hete
szülő
commit
c735118ad2

+ 5 - 5
packages/geo-areas/src/api/areas/index.ts

@@ -2,6 +2,7 @@ import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
 import { AreaService } from '../../modules/areas/area.service';
 import { AreaLevel } from '../../modules/areas/area.entity';
+import { AppDataSource } from '@d8d/shared-utils';
 
 // 省份查询参数Schema
 const getProvincesSchema = z.object({
@@ -186,9 +187,7 @@ const app = new OpenAPIHono()
   .openapi(getProvincesRoute, async (c) => {
     try {
       const { page, pageSize } = c.req.valid('query');
-      // 注意:AreaService 现在需要传入数据源,这里需要由调用方提供
-      // 在实际使用中,需要通过依赖注入或其他方式提供数据源
-      const areaService = new AreaService(c.var.dataSource);
+      const areaService = new AreaService(AppDataSource);
 
       // 获取所有省份数据
       const provinces = await areaService.getAreaTreeByLevel(AreaLevel.PROVINCE);
@@ -222,7 +221,7 @@ const app = new OpenAPIHono()
   .openapi(getCitiesRoute, async (c) => {
     try {
       const { provinceId, page, pageSize } = c.req.valid('query');
-      const areaService = new AreaService(c.var.dataSource);
+      const areaService = new AreaService(AppDataSource);
 
       // 获取指定省份下的所有城市
       const allCities = await areaService.getAreaTreeByLevel(AreaLevel.CITY);
@@ -257,7 +256,7 @@ const app = new OpenAPIHono()
   .openapi(getDistrictsRoute, async (c) => {
     try {
       const { cityId, page, pageSize } = c.req.valid('query');
-      const areaService = new AreaService(c.var.dataSource);
+      const areaService = new AreaService(AppDataSource);
 
       // 获取指定城市下的所有区县
       const allDistricts = await areaService.getAreaTreeByLevel(AreaLevel.DISTRICT);
@@ -290,4 +289,5 @@ const app = new OpenAPIHono()
     }
   });
 
+export const areaRoutes = app;
 export default app;

+ 349 - 0
packages/geo-areas/tests/integration/areas.integration.test.ts

@@ -0,0 +1,349 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooksWithEntities,
+  IntegrationTestAssertions
+} from '@d8d/shared-test-util';
+import { areaRoutes } from '../../src/api/areas';
+import { AreaEntity, AreaLevel } from '../../src/modules/areas/area.entity';
+import { DisabledStatus } from '@d8d/shared-types';
+import { TestDataFactory } from '../utils/test-data-factory';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([AreaEntity])
+
+describe('区域API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof areaRoutes>>;
+  let testAreas: AreaEntity[];
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(areaRoutes);
+
+    // 创建测试数据
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建启用状态的省份
+    const province1 = await TestDataFactory.createTestArea(dataSource, {
+      name: '北京市',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const province2 = await TestDataFactory.createTestArea(dataSource, {
+      name: '上海市',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const province3 = await TestDataFactory.createTestArea(dataSource, {
+      name: '广东省',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建启用状态的城市
+    const city11 = await TestDataFactory.createTestArea(dataSource, {
+      name: '北京市',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city12 = await TestDataFactory.createTestArea(dataSource, {
+      name: '朝阳区',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city13 = await TestDataFactory.createTestArea(dataSource, {
+      name: '海淀区',
+      level: AreaLevel.CITY,
+      parentId: province1.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city21 = await TestDataFactory.createTestArea(dataSource, {
+      name: '上海市',
+      level: AreaLevel.CITY,
+      parentId: province2.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const city22 = await TestDataFactory.createTestArea(dataSource, {
+      name: '浦东新区',
+      level: AreaLevel.CITY,
+      parentId: province2.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建启用状态的区县
+    const district101 = await TestDataFactory.createTestArea(dataSource, {
+      name: '朝阳区',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district102 = await TestDataFactory.createTestArea(dataSource, {
+      name: '海淀区',
+      level: AreaLevel.DISTRICT,
+      parentId: city13.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district103 = await TestDataFactory.createTestArea(dataSource, {
+      name: '西城区',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+    const district201 = await TestDataFactory.createTestArea(dataSource, {
+      name: '浦东新区',
+      level: AreaLevel.DISTRICT,
+      parentId: city22.id,
+      isDisabled: DisabledStatus.ENABLED
+    });
+
+    // 创建禁用状态的区域用于测试过滤
+    const disabledProvince = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用省份',
+      level: AreaLevel.PROVINCE,
+      isDisabled: DisabledStatus.DISABLED
+    });
+    const disabledCity = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用城市',
+      level: AreaLevel.CITY,
+      parentId: province3.id,
+      isDisabled: DisabledStatus.DISABLED
+    });
+    const disabledDistrict = await TestDataFactory.createTestArea(dataSource, {
+      name: '禁用区县',
+      level: AreaLevel.DISTRICT,
+      parentId: city12.id,
+      isDisabled: DisabledStatus.DISABLED
+    });
+
+    testAreas = [
+      province1, province2, province3,
+      city11, city12, city13, city21, city22,
+      district101, district102, district103, district201,
+      disabledProvince, disabledCity, disabledDistrict
+    ];
+  });
+
+  describe('GET /areas/provinces', () => {
+    it('应该成功获取启用状态的省份列表', async () => {
+      const response = await client.provinces.$get({
+        query: { page: 1, pageSize: 50 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('provinces');
+        expect(data.data).toHaveProperty('pagination');
+
+        // 验证只返回启用状态的省份
+        const provinces = data.data.provinces;
+        expect(provinces).toHaveLength(3); // 只返回3个启用状态的省份
+
+        // 验证不包含禁用状态的省份
+        const disabledProvince = provinces.find((p: any) => p.isDisabled === DisabledStatus.DISABLED);
+        expect(disabledProvince).toBeUndefined();
+
+        // 验证分页信息
+        expect(data.data.pagination).toEqual({
+          page: 1,
+          pageSize: 50,
+          total: 3,
+          totalPages: 1
+        });
+      }
+    });
+
+    it('应该正确处理分页参数', async () => {
+      const response = await client.provinces.$get({
+        query: { page: 1, pageSize: 2 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证分页结果
+        expect(data.data.provinces).toHaveLength(2);
+        expect(data.data.pagination).toEqual({
+          page: 1,
+          pageSize: 2,
+          total: 3,
+          totalPages: 2
+        });
+      }
+    });
+  });
+
+  describe('GET /areas/cities', () => {
+    it('应该成功获取指定省份下启用状态的城市列表', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: testAreas[0].id, page: 1, pageSize: 50 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('cities');
+
+        // 验证只返回启用状态的城市
+        const cities = data.data.cities;
+        expect(cities).toHaveLength(3); // 北京市下有3个启用状态的城市
+
+        // 验证城市数据正确
+        const cityNames = cities.map((c: any) => c.name);
+        expect(cityNames).toContain('北京市');
+        expect(cityNames).toContain('朝阳区');
+        expect(cityNames).toContain('海淀区');
+
+        // 验证不包含禁用状态的城市
+        const disabledCity = cities.find((c: any) => c.name === '禁用城市');
+        expect(disabledCity).toBeUndefined();
+      }
+    });
+
+    it('应该处理不存在的省份ID', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: 999, page: 1, pageSize: 50 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 不存在的省份应该返回空数组
+        expect(data.data.cities).toHaveLength(0);
+      }
+    });
+
+    it('应该验证省份ID参数', async () => {
+      const response = await client.cities.$get({
+        query: { provinceId: 0, page: 1, pageSize: 50 }
+      });
+
+      // 参数验证应该返回400错误
+      IntegrationTestAssertions.expectStatus(response, 400);
+    });
+  });
+
+  describe('GET /areas/districts', () => {
+    it('应该成功获取指定城市下启用状态的区县列表', async () => {
+      // 找到朝阳区城市对象
+      const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
+      expect(chaoyangCity).toBeDefined();
+
+      const response = await client.districts.$get({
+        query: { cityId: chaoyangCity!.id, page: 1, pageSize: 50 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 验证响应数据格式
+        expect(data).toHaveProperty('success', true);
+        expect(data).toHaveProperty('data');
+        expect(data.data).toHaveProperty('districts');
+
+        // 验证只返回启用状态的区县
+        const districts = data.data.districts;
+        expect(districts).toHaveLength(2); // 朝阳区下有2个启用状态的区县
+
+        // 验证区县数据正确
+        const districtNames = districts.map((d: any) => d.name);
+        expect(districtNames).toContain('朝阳区');
+        expect(districtNames).toContain('西城区');
+
+        // 验证不包含禁用状态的区县
+        const disabledDistrict = districts.find((d: any) => d.name === '禁用区县');
+        expect(disabledDistrict).toBeUndefined();
+      }
+    });
+
+    it('应该处理不存在的城市ID', async () => {
+      const response = await client.districts.$get({
+        query: { cityId: 999, page: 1, pageSize: 50 }
+      });
+
+      IntegrationTestAssertions.expectStatus(response, 200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+
+        // 不存在的城市应该返回空数组
+        expect(data.data.districts).toHaveLength(0);
+      }
+    });
+
+    it('应该验证城市ID参数', async () => {
+      const response = await client.districts.$get({
+        query: { cityId: 0, page: 1, pageSize: 50 }
+      });
+
+      // 参数验证应该返回400错误
+      IntegrationTestAssertions.expectStatus(response, 400);
+    });
+  });
+
+  describe('过滤禁用状态验证', () => {
+    it('应该确保所有API只返回启用状态的区域', async () => {
+      // 测试省份API
+      const provincesResponse = await client.provinces.$get({
+        query: { page: 1, pageSize: 50 }
+      });
+      IntegrationTestAssertions.expectStatus(provincesResponse, 200);
+      const provincesData = await provincesResponse.json();
+
+      // 验证省份不包含禁用状态
+      if ('data' in provincesData) {
+        const provinces = provincesData.data.provinces;
+        const hasDisabledProvince = provinces.some((p: any) => p.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledProvince).toBe(false);
+      }
+
+      // 测试城市API
+      const citiesResponse = await client.cities.$get({
+        query: { provinceId: testAreas[0].id, page: 1, pageSize: 50 }
+      });
+      IntegrationTestAssertions.expectStatus(citiesResponse, 200);
+      const citiesData = await citiesResponse.json();
+
+      // 验证城市不包含禁用状态
+      if ('data' in citiesData) {
+        const cities = citiesData.data.cities;
+        const hasDisabledCity = cities.some((c: any) => c.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledCity).toBe(false);
+      }
+
+      // 测试区县API
+      const chaoyangCity = testAreas.find(area => area.name === '朝阳区' && area.level === AreaLevel.CITY);
+      const districtsResponse = await client.districts.$get({
+        query: { cityId: chaoyangCity!.id, page: 1, pageSize: 50 }
+      });
+      IntegrationTestAssertions.expectStatus(districtsResponse, 200);
+      const districtsData = await districtsResponse.json();
+
+      // 验证区县不包含禁用状态
+      if ('data' in districtsData) {
+        const districts = districtsData.data.districts;
+        const hasDisabledDistrict = districts.some((d: any) => d.isDisabled === DisabledStatus.DISABLED);
+        expect(hasDisabledDistrict).toBe(false);
+      }
+    });
+  });
+});

+ 49 - 0
packages/geo-areas/tests/utils/test-data-factory.ts

@@ -0,0 +1,49 @@
+import { DataSource } from 'typeorm';
+import { AreaEntity, AreaLevel } from '../../src/modules/areas/area.entity';
+
+/**
+ * 测试数据工厂类 - 专门用于地区模块测试
+ */
+export class TestDataFactory {
+  /**
+   * 创建测试区域数据
+   */
+  static createAreaData(overrides: Partial<AreaEntity> = {}): Partial<AreaEntity> {
+    const timestamp = Date.now();
+    return {
+      name: `测试区域_${timestamp}`,
+      code: `area_${timestamp}`,
+      level: AreaLevel.PROVINCE,
+      parentId: null,
+      isDisabled: 0,
+      isDeleted: 0,
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试区域
+   */
+  static async createTestArea(dataSource: DataSource, overrides: Partial<AreaEntity> = {}): Promise<AreaEntity> {
+    const areaData = this.createAreaData(overrides);
+    const areaRepository = dataSource.getRepository(AreaEntity);
+
+    // 对于顶级区域(省/直辖市),parentId应该为null
+    if (areaData.level === AreaLevel.PROVINCE) {
+      areaData.parentId = null;
+    }
+    // 对于市级区域,确保有对应的省级区域
+    else if (areaData.level === AreaLevel.CITY && !areaData.parentId) {
+      const province = await this.createTestArea(dataSource, { level: AreaLevel.PROVINCE });
+      areaData.parentId = province.id;
+    }
+    // 对于区县级区域,确保有对应的市级区域
+    else if (areaData.level === AreaLevel.DISTRICT && !areaData.parentId) {
+      const city = await this.createTestArea(dataSource, { level: AreaLevel.CITY });
+      areaData.parentId = city.id;
+    }
+
+    const area = areaRepository.create(areaData);
+    return await areaRepository.save(area);
+  }
+}