import { describe, it, expect, beforeEach } from 'vitest'; import { testClient } from 'hono/testing'; import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util'; import { JWTUtil } from '@d8d/shared-utils'; import { UserEntity, Role } from '@d8d/user-module'; import { File } from '@d8d/file-module'; import { AreaEntity } from '@d8d/geo-areas'; import salaryRoutes from '../../src/routes/salary.routes'; import { SalaryLevel } from '../../src/entities/salary-level.entity'; // 设置集成测试钩子 setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, AreaEntity, SalaryLevel]) describe('薪资管理API集成测试', () => { let client: ReturnType>; let testToken: string; let testUser: UserEntity; let testProvince: AreaEntity; let testCity: AreaEntity; let testDistrict: AreaEntity; let testProvince2: AreaEntity; let testCity2: AreaEntity; beforeEach(async () => { // 创建测试客户端 client = testClient(salaryRoutes); // 获取数据源 const dataSource = await IntegrationTestDatabase.getDataSource(); // 创建测试用户 const userRepository = dataSource.getRepository(UserEntity); testUser = userRepository.create({ username: `test_user_${Date.now()}`, password: 'test_password', nickname: '测试用户', registrationSource: 'web' }); await userRepository.save(testUser); // 生成测试用户的token testToken = JWTUtil.generateToken({ id: testUser.id, username: testUser.username, roles: [{name:'user'}] }); // 创建测试区域数据 const areaRepository = dataSource.getRepository(AreaEntity); // 创建省份 testProvince = areaRepository.create({ code: '110000', name: '北京市', level: 1, parentId: null }); await areaRepository.save(testProvince); // 创建城市 testCity = areaRepository.create({ code: '110100', name: '北京市', level: 2, parentId: testProvince.id }); await areaRepository.save(testCity); // 创建区县 testDistrict = areaRepository.create({ code: '110101', name: '东城区', level: 3, parentId: testCity.id }); await areaRepository.save(testDistrict); // 创建第二个省份 testProvince2 = areaRepository.create({ code: '120000', name: '天津市', level: 1, parentId: null }); await areaRepository.save(testProvince2); // 创建第二个城市 testCity2 = areaRepository.create({ code: '120100', name: '天津市', level: 2, parentId: testProvince2.id }); await areaRepository.save(testCity2); }); describe('POST /salary/create', () => { it('应该成功创建薪资水平', async () => { const createData = { provinceId: testProvince.id, cityId: testCity.id, districtId: testDistrict.id, basicSalary: 5000.00 }; console.debug('发送的创建数据:', JSON.stringify(createData, null, 2)); const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); if (response.status !== 200) { const error = await response.json(); console.debug('创建薪资失败:', JSON.stringify(error, null, 2)); } expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.id).toBeDefined(); expect(data.provinceId).toBe(testProvince.id); expect(data.cityId).toBe(testCity.id); expect(data.districtId).toBe(testDistrict.id); expect(data.basicSalary).toBe(5000.00); expect(data.totalSalary).toBe(5000.00); // 只有基本工资 } }); it('应该验证区域层级关系', async () => { const createData = { provinceId: testProvince.id, cityId: testDistrict.id, // 错误的层级:城市ID使用了区县ID districtId: testCity.id, // 错误的层级:区县ID使用了城市ID basicSalary: 5000.00 }; const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); }); it('应该验证中文错误消息 - 缺少必填字段', async () => { const createData = { provinceId: 0, // 发送0而不是null cityId: 0, // 发送0而不是null basicSalary: 5000.00 }; const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); if (response.status === 400) { const errorData = await response.json() as any; console.debug('缺少必填字段错误响应:', JSON.stringify(errorData, null, 2)); // 验证错误响应格式 - 根据实际响应调整 if ('success' in errorData) { // 格式: { success: boolean; error: { name: string; message: string } } expect(errorData.success).toBe(false); expect(errorData.error).toHaveProperty('name'); expect(errorData.error.name).toBe('ZodError'); expect(errorData.error).toHaveProperty('message'); // 解析错误消息 const errorMessages = JSON.parse(errorData.error.message); expect(Array.isArray(errorMessages)).toBe(true); // 验证具体的错误消息 const errorTexts = errorMessages.map((err: any) => err.message); expect(errorTexts).toContain('请选择省份'); expect(errorTexts).toContain('请选择城市'); } else { // 格式: { code: number; message: string; errors?: any } expect(errorData.code).toBe(400); expect(errorData.message).toBe('参数错误'); expect(errorData).toHaveProperty('errors'); expect(Array.isArray(errorData.errors)).toBe(true); // 验证具体的错误消息 const errorTexts = errorData.errors.map((err: any) => err.message); expect(errorTexts).toContain('请选择省份'); expect(errorTexts).toContain('请选择城市'); } } }); it('应该验证中文错误消息 - 基本工资为0', async () => { const createData = { provinceId: testProvince.id, cityId: testCity.id, basicSalary: 0 // 基本工资为0,应该触发错误 }; const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); if (response.status === 400) { const errorData = await response.json() as any; console.debug('基本工资为0错误响应:', JSON.stringify(errorData, null, 2)); // 验证错误响应格式 - 支持两种格式 if (errorData.success !== undefined) { // 格式: { success: false, error: { name: 'ZodError', message: '...' } } expect(errorData.success).toBe(false); expect(errorData).toHaveProperty('error'); expect(errorData.error).toHaveProperty('name'); expect(errorData.error.name).toBe('ZodError'); expect(errorData.error).toHaveProperty('message'); // 解析错误消息 const errorMessages = JSON.parse(errorData.error.message); expect(Array.isArray(errorMessages)).toBe(true); // 验证具体的错误消息 const errorTexts = errorMessages.map((err: any) => err.message); expect(errorTexts).toContain('基本工资必须大于0'); } else { // 格式: { code: number; message: string; errors?: any } expect(errorData.code).toBe(400); expect(errorData.message).toBe('参数错误'); expect(errorData).toHaveProperty('errors'); expect(Array.isArray(errorData.errors)).toBe(true); // 验证具体的错误消息 const errorTexts = errorData.errors.map((err: any) => err.message); expect(errorTexts).toContain('基本工资必须大于0'); } } }); it('应该验证中文错误消息 - 津贴补贴为负数', async () => { const createData = { provinceId: testProvince.id, cityId: testCity.id, basicSalary: 5000.00, allowance: -100 // 津贴补贴为负数,应该触发错误 }; const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); if (response.status === 400) { const errorData = await response.json() as any; console.debug('津贴补贴为负数错误响应:', JSON.stringify(errorData, null, 2)); // 验证错误响应格式 - 支持两种格式 if (errorData.success !== undefined) { // 格式: { success: false, error: { name: 'ZodError', message: '...' } } expect(errorData.success).toBe(false); expect(errorData).toHaveProperty('error'); expect(errorData.error).toHaveProperty('name'); expect(errorData.error.name).toBe('ZodError'); expect(errorData.error).toHaveProperty('message'); // 解析错误消息 const errorMessages = JSON.parse(errorData.error.message); expect(Array.isArray(errorMessages)).toBe(true); // 验证具体的错误消息 const errorTexts = errorMessages.map((err: any) => err.message); expect(errorTexts).toContain('津贴补贴不能为负数'); } else { // 格式: { code: number; message: string; errors?: any } expect(errorData.code).toBe(400); expect(errorData.message).toBe('参数错误'); expect(errorData).toHaveProperty('errors'); expect(Array.isArray(errorData.errors)).toBe(true); // 验证具体的错误消息 const errorTexts = errorData.errors.map((err: any) => err.message); expect(errorTexts).toContain('津贴补贴不能为负数'); } } }); it('应该验证中文错误消息 - 类型转换错误', async () => { const createData = { provinceId: testProvince.id, cityId: testCity.id, basicSalary: '不是数字' as any, // 字符串,应该触发类型转换错误 allowance: 100 }; const response = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); if (response.status === 400) { const errorData = await response.json() as any; console.debug('类型转换错误响应:', JSON.stringify(errorData, null, 2)); // 验证错误响应格式 - 支持两种格式 if (errorData.success !== undefined) { // 格式: { success: false, error: { name: 'ZodError', message: '...' } } expect(errorData.success).toBe(false); expect(errorData).toHaveProperty('error'); expect(errorData.error).toHaveProperty('name'); expect(errorData.error.name).toBe('ZodError'); expect(errorData.error).toHaveProperty('message'); // 解析错误消息 const errorMessages = JSON.parse(errorData.error.message); expect(Array.isArray(errorMessages)).toBe(true); // 验证具体的错误消息 const errorTexts = errorMessages.map((err: any) => err.message); console.debug('类型转换错误消息:', errorTexts); // 类型转换错误显示我们设置的中文错误消息 expect(errorTexts).toContain('基本工资必须大于0'); } else { // 格式: { code: number; message: string; errors?: any } expect(errorData.code).toBe(400); expect(errorData.message).toBe('参数错误'); expect(errorData).toHaveProperty('errors'); expect(Array.isArray(errorData.errors)).toBe(true); // 验证具体的错误消息 const errorTexts = errorData.errors.map((err: any) => err.message); console.debug('类型转换错误消息:', errorTexts); expect(errorTexts).toContain('基本工资必须大于0'); } } }); it('应该验证区域唯一性约束', async () => { // 第一次创建 const createData = { provinceId: testProvince.id, cityId: testCity.id, basicSalary: 5000.00 }; const firstResponse = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(firstResponse.status).toBe(200); // 第二次创建相同区域 const secondResponse = await client.create.$post({ json: createData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(secondResponse.status).toBe(400); }); }); describe('GET /salary/list', () => { beforeEach(async () => { // 创建测试数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); const salary1 = salaryRepository.create({ provinceId: testProvince.id, cityId: testCity.id, districtId: testDistrict.id, basicSalary: 5000.00, allowance: 1000.00, insurance: 500.00, housingFund: 800.00, totalSalary: 7300.00 }); await salaryRepository.save(salary1); const salary2 = salaryRepository.create({ provinceId: testProvince2.id, // 使用不同的省份 cityId: testCity2.id, // 使用不同的城市 basicSalary: 6000.00, allowance: 1200.00, insurance: 600.00, housingFund: 900.00, totalSalary: 8700.00 }); await salaryRepository.save(salary2); }); it('应该返回所有薪资水平列表', async () => { const response = await client.list.$get({ query: {} }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data).toHaveProperty('data'); expect(data).toHaveProperty('total'); expect(data.data).toHaveLength(2); expect(data.total).toBe(2); } }); it('应该支持按区域ID过滤', async () => { const response = await client.list.$get({ query: { provinceId: testProvince.id, cityId: testCity.id } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.data).toHaveLength(1); } }); it('应该支持分页查询', async () => { const response = await client.list.$get({ query: { skip: 0, take: 1 } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.data).toHaveLength(1); expect(data.total).toBe(2); } }); }); describe('GET /salary/detail/:id', () => { let testSalary: SalaryLevel; beforeEach(async () => { // 创建测试薪资数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); testSalary = salaryRepository.create({ provinceId: testProvince.id, cityId: testCity.id, districtId: testDistrict.id, basicSalary: 5000.00, allowance: 1000.00, insurance: 500.00, housingFund: 800.00, totalSalary: 7300.00, }); await salaryRepository.save(testSalary); }); it('应该返回指定ID的薪资详情', async () => { const response = await client.detail[':id'].$get({ param: { id: testSalary.id.toString() } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data?.id).toBe(testSalary.id); expect(data?.provinceId).toBe(testProvince.id); expect(data?.cityId).toBe(testCity.id); } }); it('应该处理不存在的薪资ID', async () => { const response = await client.detail[':id'].$get({ param: { id: 999999 } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(404); }); }); describe('GET /salary/byProvinceCity', () => { let testSalary: SalaryLevel; beforeEach(async () => { // 创建测试薪资数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); testSalary = salaryRepository.create({ provinceId: testProvince.id, cityId: testCity.id, basicSalary: 5000.00, allowance: 1000.00, insurance: 500.00, housingFund: 800.00, totalSalary: 7300.00, }); await salaryRepository.save(testSalary); }); it('应该按省份城市查询薪资', async () => { const response = await client.byProvinceCity.$get({ query: { provinceId: testProvince.id, cityId: testCity.id } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data?.id).toBe(testSalary.id); expect(data?.provinceId).toBe(testProvince.id); expect(data?.cityId).toBe(testCity.id); } }); it('应该处理不存在的区域组合', async () => { const response = await client.byProvinceCity.$get({ query: { provinceId: 999999, cityId: 999999 } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(404); }); }); describe('PUT /salary/update/:id', () => { let testSalary: SalaryLevel; beforeEach(async () => { // 创建测试薪资数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); testSalary = salaryRepository.create({ provinceId: testProvince.id, cityId: testCity.id, basicSalary: 5000.00, allowance: 1000.00, insurance: 500.00, housingFund: 800.00, totalSalary: 7300.00, }); await salaryRepository.save(testSalary); }); it('应该成功更新薪资水平', async () => { const updateData = { basicSalary: 5500.00, allowance: 1200.00 }; const response = await client.update[':id'].$put({ param: { id: testSalary.id.toString() }, json: updateData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data.id).toBe(testSalary.id); expect(data.basicSalary).toBe(5500.00); expect(data.allowance).toBe(1200.00); expect(data.totalSalary).toBe(8000.00); // 5500 + 1200 + 500 + 800 } }); it('应该验证更新后的区域数据', async () => { const updateData = { provinceId: 999999 // 不存在的区域ID }; const response = await client.update[':id'].$put({ param: { id: testSalary.id.toString() }, json: updateData }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(400); }); }); describe('DELETE /salary/delete/:id', () => { let testSalary: SalaryLevel; beforeEach(async () => { // 创建测试薪资数据 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); testSalary = salaryRepository.create({ provinceId: testProvince.id, cityId: testCity.id, basicSalary: 5000.00, allowance: 1000.00, insurance: 500.00, housingFund: 800.00, totalSalary: 7300.00, }); await salaryRepository.save(testSalary); }); it('应该成功删除薪资水平', async () => { const response = await client.delete[':id'].$delete({ param: { id: testSalary.id.toString() } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(200); if (response.status === 200) { const data = await response.json(); expect(data).toEqual({ success: true }); // 验证数据已删除 const dataSource = await IntegrationTestDatabase.getDataSource(); const salaryRepository = dataSource.getRepository(SalaryLevel); const deletedSalary = await salaryRepository.findOne({ where: { id: testSalary.id } }); expect(deletedSalary).toBeNull(); } }); it('应该处理不存在的薪资ID', async () => { const response = await client.delete[':id'].$delete({ param: { id: 999999 } }, { headers: { 'Authorization': `Bearer ${testToken}` } }); expect(response.status).toBe(404); }); }); describe('认证测试', () => { it('应该要求认证', async () => { const response = await client.list.$get({ query: {} // 不提供Authorization header }); expect(response.status).toBe(401); }); it('应该验证无效token', async () => { const response = await client.list.$get({ query: {} }, { headers: { 'Authorization': 'Bearer invalid_token' } }); expect(response.status).toBe(401); }); }); });