|
|
@@ -0,0 +1,423 @@
|
|
|
+import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
+import {
|
|
|
+ IntegrationTestDatabase,
|
|
|
+ setupIntegrationDatabaseHooksWithEntities
|
|
|
+} from '@d8d/shared-test-util';
|
|
|
+import { IntegrationTestAssertions } from '../utils/integration-test-utils';
|
|
|
+import adminAreaRoutes from '../../src/api/admin/areas';
|
|
|
+import { AreaEntity, AreaLevel } from '../../src/modules/areas/area.entity';
|
|
|
+import { DisabledStatus } from '@d8d/shared-types';
|
|
|
+import { TestDataFactory } from '../utils/test-data-factory';
|
|
|
+import { AuthService } from '@d8d/auth-module';
|
|
|
+import { UserService } from '@d8d/user-module';
|
|
|
+
|
|
|
+// 设置集成测试钩子
|
|
|
+setupIntegrationDatabaseHooksWithEntities([AreaEntity])
|
|
|
+
|
|
|
+describe('管理地区API集成测试 (使用hono/testing)', () => {
|
|
|
+ let client: ReturnType<typeof testClient<typeof adminAreaRoutes>>;
|
|
|
+ let authService: AuthService;
|
|
|
+ let userService: UserService;
|
|
|
+ let testToken: string;
|
|
|
+ let testUser: any;
|
|
|
+ let testAreas: AreaEntity[];
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 创建测试客户端
|
|
|
+ client = testClient(adminAreaRoutes);
|
|
|
+
|
|
|
+ // 获取数据源
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 初始化服务
|
|
|
+ userService = new UserService(dataSource);
|
|
|
+ authService = new AuthService(userService);
|
|
|
+
|
|
|
+ // 创建测试用户并生成token
|
|
|
+ testUser = await TestDataFactory.createTestUser(dataSource, {
|
|
|
+ username: 'testuser_admin_areas',
|
|
|
+ password: 'TestPassword123!',
|
|
|
+ email: 'testuser_admin_areas@example.com'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 生成测试用户的token
|
|
|
+ testToken = authService.generateToken(testUser);
|
|
|
+
|
|
|
+ // 创建测试地区数据
|
|
|
+ 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 city1 = await TestDataFactory.createTestArea(dataSource, {
|
|
|
+ name: '朝阳区',
|
|
|
+ level: AreaLevel.CITY,
|
|
|
+ parentId: province1.id,
|
|
|
+ isDisabled: DisabledStatus.ENABLED
|
|
|
+ });
|
|
|
+ const city2 = await TestDataFactory.createTestArea(dataSource, {
|
|
|
+ name: '海淀区',
|
|
|
+ level: AreaLevel.CITY,
|
|
|
+ parentId: province1.id,
|
|
|
+ isDisabled: DisabledStatus.ENABLED
|
|
|
+ });
|
|
|
+ const district1 = await TestDataFactory.createTestArea(dataSource, {
|
|
|
+ name: '朝阳区',
|
|
|
+ level: AreaLevel.DISTRICT,
|
|
|
+ parentId: city1.id,
|
|
|
+ isDisabled: DisabledStatus.ENABLED
|
|
|
+ });
|
|
|
+
|
|
|
+ testAreas = [province1, province2, city1, city2, district1];
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('认证测试', () => {
|
|
|
+ it('应该拒绝无认证令牌的请求', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ });
|
|
|
+
|
|
|
+ // 应该返回401状态码,因为缺少认证
|
|
|
+ expect(response.status).toBe(401);
|
|
|
+ if (response.status === 401) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.message).toContain('Authorization header missing');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该拒绝无效认证令牌的请求', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer invalid.token.here'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 应该返回401状态码,因为令牌无效
|
|
|
+ expect(response.status).toBe(401);
|
|
|
+ if (response.status === 401) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.message).toContain('Invalid token');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('地区CRUD操作测试', () => {
|
|
|
+ it('应该成功创建地区(使用有效认证令牌)', async () => {
|
|
|
+ const areaData = {
|
|
|
+ name: '测试省份',
|
|
|
+ code: 'test_province_001',
|
|
|
+ level: AreaLevel.PROVINCE,
|
|
|
+ parentId: null,
|
|
|
+ isDisabled: DisabledStatus.ENABLED
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client.index.$post({
|
|
|
+ json: areaData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 断言响应
|
|
|
+ expect(response.status).toBe(201);
|
|
|
+ if (response.status === 201) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData).toHaveProperty('id');
|
|
|
+ expect(responseData.name).toBe(areaData.name);
|
|
|
+ expect(responseData.code).toBe(areaData.code);
|
|
|
+ expect(responseData.level).toBe(areaData.level);
|
|
|
+
|
|
|
+ // 断言数据库中存在地区
|
|
|
+ await IntegrationTestAssertions.expectAreaToExist(areaData.name);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该成功获取地区列表', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(Array.isArray(responseData.data)).toBe(true);
|
|
|
+ expect(responseData.data.length).toBeGreaterThanOrEqual(5); // 至少有5个测试地区
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该成功获取单个地区详情', async () => {
|
|
|
+ const testArea = testAreas[0];
|
|
|
+
|
|
|
+ const response = await client[':id'].$get({
|
|
|
+ param: { id: testArea.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.id).toBe(testArea.id);
|
|
|
+ expect(responseData.name).toBe(testArea.name);
|
|
|
+ expect(responseData.level).toBe(testArea.level);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该返回404当地区不存在时', async () => {
|
|
|
+ const response = await client[':id'].$get({
|
|
|
+ param: { id: 999999 }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(404);
|
|
|
+ if (response.status === 404) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.message).toContain('资源不存在');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该成功更新地区信息', async () => {
|
|
|
+ const testArea = testAreas[0];
|
|
|
+ const updateData = {
|
|
|
+ name: '更新后的北京市',
|
|
|
+ code: 'updated_beijing'
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client[':id'].$put({
|
|
|
+ param: { id: testArea.id },
|
|
|
+ json: updateData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.name).toBe(updateData.name);
|
|
|
+ expect(responseData.code).toBe(updateData.code);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该成功删除地区', async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ const testArea = await TestDataFactory.createTestArea(dataSource, {
|
|
|
+ name: '待删除地区',
|
|
|
+ level: AreaLevel.PROVINCE
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await client[':id'].$delete({
|
|
|
+ param: { id: testArea.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 204);
|
|
|
+
|
|
|
+ // 验证地区已从数据库中删除
|
|
|
+ await IntegrationTestAssertions.expectAreaNotToExist('待删除地区');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('地区搜索测试', () => {
|
|
|
+ it('应该能够按名称搜索地区', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: { keyword: '北京' }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(Array.isArray(responseData.data)).toBe(true);
|
|
|
+ expect(responseData.data.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 验证搜索结果包含正确的地区
|
|
|
+ const names = responseData.data.map((area: any) => area.name);
|
|
|
+ expect(names).toContain('北京市');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该能够按代码搜索地区', async () => {
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: { keyword: 'area_' }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.data.length).toBeGreaterThan(0);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('树形结构查询测试', () => {
|
|
|
+ it('应该成功获取完整树形结构', async () => {
|
|
|
+ const response = await client.tree.$get({}, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData).toHaveProperty('success', true);
|
|
|
+ expect(responseData).toHaveProperty('data');
|
|
|
+ expect(Array.isArray(responseData.data)).toBe(true);
|
|
|
+
|
|
|
+ // 验证树形结构包含省份
|
|
|
+ const provinceNames = responseData.data.map((area: any) => area.name);
|
|
|
+ expect(provinceNames).toContain('北京市');
|
|
|
+ expect(provinceNames).toContain('上海市');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该根据层级获取树形结构', async () => {
|
|
|
+ const response = await client.tree.level[':level'].$get({
|
|
|
+ param: { level: AreaLevel.PROVINCE }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData).toHaveProperty('success', true);
|
|
|
+ expect(responseData).toHaveProperty('data');
|
|
|
+
|
|
|
+ // 验证只返回省级地区
|
|
|
+ const provinces = responseData.data;
|
|
|
+ expect(provinces.length).toBeGreaterThan(0);
|
|
|
+ provinces.forEach((province: any) => {
|
|
|
+ expect(province.level).toBe(AreaLevel.PROVINCE);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该获取指定节点的子树', async () => {
|
|
|
+ const province = testAreas.find(area => area.name === '北京市');
|
|
|
+ expect(province).toBeDefined();
|
|
|
+
|
|
|
+ const response = await client.tree[':id'].$get({
|
|
|
+ param: { id: province!.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData).toHaveProperty('success', true);
|
|
|
+ expect(responseData).toHaveProperty('data');
|
|
|
+
|
|
|
+ // 验证子树包含城市
|
|
|
+ const subTree = responseData.data;
|
|
|
+ expect(subTree).toHaveProperty('children');
|
|
|
+ if (subTree.children) {
|
|
|
+ const cityNames = subTree.children.map((city: any) => city.name);
|
|
|
+ expect(cityNames).toContain('朝阳区');
|
|
|
+ expect(cityNames).toContain('海淀区');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该获取区域路径', async () => {
|
|
|
+ const district = testAreas.find(area => area.level === AreaLevel.DISTRICT);
|
|
|
+ expect(district).toBeDefined();
|
|
|
+
|
|
|
+ const response = await client.path[':id'].$get({
|
|
|
+ param: { id: district!.id }
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData).toHaveProperty('success', true);
|
|
|
+ expect(responseData).toHaveProperty('data');
|
|
|
+
|
|
|
+ // 验证路径包含正确的层级
|
|
|
+ const path = responseData.data;
|
|
|
+ expect(Array.isArray(path)).toBe(true);
|
|
|
+ expect(path.length).toBeGreaterThan(0);
|
|
|
+
|
|
|
+ // 路径应该包含从省份到区县的完整路径
|
|
|
+ const levels = path.map((area: any) => area.level);
|
|
|
+ expect(levels).toContain(AreaLevel.PROVINCE);
|
|
|
+ expect(levels).toContain(AreaLevel.CITY);
|
|
|
+ expect(levels).toContain(AreaLevel.DISTRICT);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('性能测试', () => {
|
|
|
+ it('地区列表查询响应时间应小于200ms', async () => {
|
|
|
+ const startTime = Date.now();
|
|
|
+ const response = await client.index.$get({
|
|
|
+ query: {}
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const endTime = Date.now();
|
|
|
+ const responseTime = endTime - startTime;
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
|
|
|
+ });
|
|
|
+
|
|
|
+ it('树形结构查询响应时间应小于300ms', async () => {
|
|
|
+ const startTime = Date.now();
|
|
|
+ const response = await client.tree.$get({}, {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const endTime = Date.now();
|
|
|
+ const responseTime = endTime - startTime;
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ expect(responseTime).toBeLessThan(300); // 树形结构查询响应时间应小于300ms
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|