salary.integration.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. import { describe, it, expect, beforeEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
  4. import { JWTUtil } from '@d8d/shared-utils';
  5. import { UserEntity, Role } from '@d8d/user-module';
  6. import { File } from '@d8d/file-module';
  7. import { AreaEntity } from '@d8d/geo-areas';
  8. import salaryRoutes from '../../src/routes/salary.routes';
  9. import { SalaryLevel } from '../../src/entities/salary-level.entity';
  10. // 设置集成测试钩子
  11. setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, AreaEntity, SalaryLevel])
  12. describe('薪资管理API集成测试', () => {
  13. let client: ReturnType<typeof testClient<typeof salaryRoutes>>;
  14. let testToken: string;
  15. let testUser: UserEntity;
  16. let testProvince: AreaEntity;
  17. let testCity: AreaEntity;
  18. let testDistrict: AreaEntity;
  19. let testProvince2: AreaEntity;
  20. let testCity2: AreaEntity;
  21. beforeEach(async () => {
  22. // 创建测试客户端
  23. client = testClient(salaryRoutes);
  24. // 获取数据源
  25. const dataSource = await IntegrationTestDatabase.getDataSource();
  26. // 创建测试用户
  27. const userRepository = dataSource.getRepository(UserEntity);
  28. testUser = userRepository.create({
  29. username: `test_user_${Date.now()}`,
  30. password: 'test_password',
  31. nickname: '测试用户',
  32. registrationSource: 'web'
  33. });
  34. await userRepository.save(testUser);
  35. // 生成测试用户的token
  36. testToken = JWTUtil.generateToken({
  37. id: testUser.id,
  38. username: testUser.username,
  39. roles: [{name:'user'}]
  40. });
  41. // 创建测试区域数据
  42. const areaRepository = dataSource.getRepository(AreaEntity);
  43. // 创建省份
  44. testProvince = areaRepository.create({
  45. code: '110000',
  46. name: '北京市',
  47. level: 1,
  48. parentId: null
  49. });
  50. await areaRepository.save(testProvince);
  51. // 创建城市
  52. testCity = areaRepository.create({
  53. code: '110100',
  54. name: '北京市',
  55. level: 2,
  56. parentId: testProvince.id
  57. });
  58. await areaRepository.save(testCity);
  59. // 创建区县
  60. testDistrict = areaRepository.create({
  61. code: '110101',
  62. name: '东城区',
  63. level: 3,
  64. parentId: testCity.id
  65. });
  66. await areaRepository.save(testDistrict);
  67. // 创建第二个省份
  68. testProvince2 = areaRepository.create({
  69. code: '120000',
  70. name: '天津市',
  71. level: 1,
  72. parentId: null
  73. });
  74. await areaRepository.save(testProvince2);
  75. // 创建第二个城市
  76. testCity2 = areaRepository.create({
  77. code: '120100',
  78. name: '天津市',
  79. level: 2,
  80. parentId: testProvince2.id
  81. });
  82. await areaRepository.save(testCity2);
  83. });
  84. describe('POST /salary/create', () => {
  85. it('应该成功创建薪资水平', async () => {
  86. const createData = {
  87. provinceId: testProvince.id,
  88. cityId: testCity.id,
  89. districtId: testDistrict.id,
  90. basicSalary: 5000.00
  91. };
  92. console.debug('发送的创建数据:', JSON.stringify(createData, null, 2));
  93. const response = await client.create.$post({
  94. json: createData
  95. }, {
  96. headers: {
  97. 'Authorization': `Bearer ${testToken}`
  98. }
  99. });
  100. if (response.status !== 200) {
  101. const error = await response.json();
  102. console.debug('创建薪资失败:', JSON.stringify(error, null, 2));
  103. }
  104. expect(response.status).toBe(200);
  105. if (response.status === 200) {
  106. const data = await response.json();
  107. expect(data.id).toBeDefined();
  108. expect(data.provinceId).toBe(testProvince.id);
  109. expect(data.cityId).toBe(testCity.id);
  110. expect(data.districtId).toBe(testDistrict.id);
  111. expect(data.basicSalary).toBe(5000.00);
  112. expect(data.totalSalary).toBe(5000.00); // 只有基本工资
  113. }
  114. });
  115. it('应该验证区域层级关系', async () => {
  116. const createData = {
  117. provinceId: testProvince.id,
  118. cityId: testDistrict.id, // 错误的层级:城市ID使用了区县ID
  119. districtId: testCity.id, // 错误的层级:区县ID使用了城市ID
  120. basicSalary: 5000.00
  121. };
  122. const response = await client.create.$post({
  123. json: createData
  124. }, {
  125. headers: {
  126. 'Authorization': `Bearer ${testToken}`
  127. }
  128. });
  129. expect(response.status).toBe(400);
  130. });
  131. it('应该验证中文错误消息 - 缺少必填字段', async () => {
  132. const createData = {
  133. provinceId: null, // 发送null而不是省略
  134. cityId: null, // 发送null而不是省略
  135. basicSalary: 5000.00
  136. };
  137. const response = await client.create.$post({
  138. json: createData
  139. }, {
  140. headers: {
  141. 'Authorization': `Bearer ${testToken}`
  142. }
  143. });
  144. expect(response.status).toBe(400);
  145. const errorData = await response.json();
  146. console.debug('缺少必填字段错误响应:', JSON.stringify(errorData, null, 2));
  147. // 验证错误响应格式
  148. expect(errorData).toHaveProperty('success');
  149. expect(errorData.success).toBe(false);
  150. expect(errorData).toHaveProperty('error');
  151. expect(errorData.error).toHaveProperty('name');
  152. expect(errorData.error.name).toBe('ZodError');
  153. expect(errorData.error).toHaveProperty('message');
  154. // 解析错误消息
  155. const errorMessages = JSON.parse(errorData.error.message);
  156. expect(Array.isArray(errorMessages)).toBe(true);
  157. // 验证具体的错误消息
  158. const errorTexts = errorMessages.map((err: any) => err.message);
  159. expect(errorTexts).toContain('请选择省份');
  160. expect(errorTexts).toContain('请选择城市');
  161. });
  162. it('应该验证中文错误消息 - 基本工资为0', async () => {
  163. const createData = {
  164. provinceId: testProvince.id,
  165. cityId: testCity.id,
  166. basicSalary: 0 // 基本工资为0,应该触发错误
  167. };
  168. const response = await client.create.$post({
  169. json: createData
  170. }, {
  171. headers: {
  172. 'Authorization': `Bearer ${testToken}`
  173. }
  174. });
  175. expect(response.status).toBe(400);
  176. const errorData = await response.json();
  177. console.debug('基本工资为0错误响应:', JSON.stringify(errorData, null, 2));
  178. // 验证错误响应格式
  179. expect(errorData).toHaveProperty('success');
  180. expect(errorData.success).toBe(false);
  181. expect(errorData).toHaveProperty('error');
  182. expect(errorData.error).toHaveProperty('name');
  183. expect(errorData.error.name).toBe('ZodError');
  184. expect(errorData.error).toHaveProperty('message');
  185. // 解析错误消息
  186. const errorMessages = JSON.parse(errorData.error.message);
  187. expect(Array.isArray(errorMessages)).toBe(true);
  188. // 验证具体的错误消息
  189. const errorTexts = errorMessages.map((err: any) => err.message);
  190. expect(errorTexts).toContain('基本工资必须大于0');
  191. });
  192. it('应该验证中文错误消息 - 津贴补贴为负数', async () => {
  193. const createData = {
  194. provinceId: testProvince.id,
  195. cityId: testCity.id,
  196. basicSalary: 5000.00,
  197. allowance: -100 // 津贴补贴为负数,应该触发错误
  198. };
  199. const response = await client.create.$post({
  200. json: createData
  201. }, {
  202. headers: {
  203. 'Authorization': `Bearer ${testToken}`
  204. }
  205. });
  206. expect(response.status).toBe(400);
  207. const errorData = await response.json();
  208. console.debug('津贴补贴为负数错误响应:', JSON.stringify(errorData, null, 2));
  209. // 验证错误响应格式
  210. expect(errorData).toHaveProperty('success');
  211. expect(errorData.success).toBe(false);
  212. expect(errorData).toHaveProperty('error');
  213. expect(errorData.error).toHaveProperty('name');
  214. expect(errorData.error.name).toBe('ZodError');
  215. expect(errorData.error).toHaveProperty('message');
  216. // 解析错误消息
  217. const errorMessages = JSON.parse(errorData.error.message);
  218. expect(Array.isArray(errorMessages)).toBe(true);
  219. // 验证具体的错误消息
  220. const errorTexts = errorMessages.map((err: any) => err.message);
  221. expect(errorTexts).toContain('津贴补贴不能为负数');
  222. });
  223. it('应该验证中文错误消息 - 类型转换错误', async () => {
  224. const createData = {
  225. provinceId: testProvince.id,
  226. cityId: testCity.id,
  227. basicSalary: '不是数字', // 字符串,应该触发类型转换错误
  228. allowance: 100
  229. };
  230. const response = await client.create.$post({
  231. json: createData
  232. }, {
  233. headers: {
  234. 'Authorization': `Bearer ${testToken}`
  235. }
  236. });
  237. expect(response.status).toBe(400);
  238. const errorData = await response.json();
  239. console.debug('类型转换错误响应:', JSON.stringify(errorData, null, 2));
  240. // 验证错误响应格式
  241. expect(errorData).toHaveProperty('success');
  242. expect(errorData.success).toBe(false);
  243. expect(errorData).toHaveProperty('error');
  244. expect(errorData.error).toHaveProperty('name');
  245. expect(errorData.error.name).toBe('ZodError');
  246. expect(errorData.error).toHaveProperty('message');
  247. // 解析错误消息
  248. const errorMessages = JSON.parse(errorData.error.message);
  249. expect(Array.isArray(errorMessages)).toBe(true);
  250. // 验证具体的错误消息
  251. const errorTexts = errorMessages.map((err: any) => err.message);
  252. console.debug('类型转换错误消息:', errorTexts);
  253. // 类型转换错误显示我们设置的中文错误消息
  254. expect(errorTexts).toContain('基本工资必须大于0');
  255. });
  256. it('应该验证区域唯一性约束', async () => {
  257. // 第一次创建
  258. const createData = {
  259. provinceId: testProvince.id,
  260. cityId: testCity.id,
  261. basicSalary: 5000.00
  262. };
  263. const firstResponse = await client.create.$post({
  264. json: createData
  265. }, {
  266. headers: {
  267. 'Authorization': `Bearer ${testToken}`
  268. }
  269. });
  270. expect(firstResponse.status).toBe(200);
  271. // 第二次创建相同区域
  272. const secondResponse = await client.create.$post({
  273. json: createData
  274. }, {
  275. headers: {
  276. 'Authorization': `Bearer ${testToken}`
  277. }
  278. });
  279. expect(secondResponse.status).toBe(400);
  280. });
  281. });
  282. describe('GET /salary/list', () => {
  283. beforeEach(async () => {
  284. // 创建测试数据
  285. const dataSource = await IntegrationTestDatabase.getDataSource();
  286. const salaryRepository = dataSource.getRepository(SalaryLevel);
  287. const salary1 = salaryRepository.create({
  288. provinceId: testProvince.id,
  289. cityId: testCity.id,
  290. districtId: testDistrict.id,
  291. basicSalary: 5000.00,
  292. allowance: 1000.00,
  293. insurance: 500.00,
  294. housingFund: 800.00,
  295. totalSalary: 7300.00
  296. });
  297. await salaryRepository.save(salary1);
  298. const salary2 = salaryRepository.create({
  299. provinceId: testProvince2.id, // 使用不同的省份
  300. cityId: testCity2.id, // 使用不同的城市
  301. basicSalary: 6000.00,
  302. allowance: 1200.00,
  303. insurance: 600.00,
  304. housingFund: 900.00,
  305. totalSalary: 8700.00
  306. });
  307. await salaryRepository.save(salary2);
  308. });
  309. it('应该返回所有薪资水平列表', async () => {
  310. const response = await client.list.$get({
  311. query: {}
  312. }, {
  313. headers: {
  314. 'Authorization': `Bearer ${testToken}`
  315. }
  316. });
  317. expect(response.status).toBe(200);
  318. if (response.status === 200) {
  319. const data = await response.json();
  320. expect(data).toHaveProperty('data');
  321. expect(data).toHaveProperty('total');
  322. expect(data.data).toHaveLength(2);
  323. expect(data.total).toBe(2);
  324. }
  325. });
  326. it('应该支持按区域ID过滤', async () => {
  327. const response = await client.list.$get({
  328. query: {
  329. provinceId: testProvince.id.toString(),
  330. cityId: testCity.id.toString()
  331. }
  332. }, {
  333. headers: {
  334. 'Authorization': `Bearer ${testToken}`
  335. }
  336. });
  337. expect(response.status).toBe(200);
  338. if (response.status === 200) {
  339. const data = await response.json();
  340. expect(data.data).toHaveLength(1);
  341. }
  342. });
  343. it('应该支持分页查询', async () => {
  344. const response = await client.list.$get({
  345. query: {
  346. skip: '0',
  347. take: '1'
  348. }
  349. }, {
  350. headers: {
  351. 'Authorization': `Bearer ${testToken}`
  352. }
  353. });
  354. expect(response.status).toBe(200);
  355. if (response.status === 200) {
  356. const data = await response.json();
  357. expect(data.data).toHaveLength(1);
  358. expect(data.total).toBe(2);
  359. }
  360. });
  361. });
  362. describe('GET /salary/detail/:id', () => {
  363. let testSalary: SalaryLevel;
  364. beforeEach(async () => {
  365. // 创建测试薪资数据
  366. const dataSource = await IntegrationTestDatabase.getDataSource();
  367. const salaryRepository = dataSource.getRepository(SalaryLevel);
  368. testSalary = salaryRepository.create({
  369. provinceId: testProvince.id,
  370. cityId: testCity.id,
  371. districtId: testDistrict.id,
  372. basicSalary: 5000.00,
  373. allowance: 1000.00,
  374. insurance: 500.00,
  375. housingFund: 800.00,
  376. totalSalary: 7300.00,
  377. });
  378. await salaryRepository.save(testSalary);
  379. });
  380. it('应该返回指定ID的薪资详情', async () => {
  381. const response = await client.detail[':id'].$get({
  382. param: { id: testSalary.id.toString() }
  383. }, {
  384. headers: {
  385. 'Authorization': `Bearer ${testToken}`
  386. }
  387. });
  388. expect(response.status).toBe(200);
  389. if (response.status === 200) {
  390. const data = await response.json();
  391. expect(data?.id).toBe(testSalary.id);
  392. expect(data?.provinceId).toBe(testProvince.id);
  393. expect(data?.cityId).toBe(testCity.id);
  394. }
  395. });
  396. it('应该处理不存在的薪资ID', async () => {
  397. const response = await client.detail[':id'].$get({
  398. param: { id: '999999' }
  399. }, {
  400. headers: {
  401. 'Authorization': `Bearer ${testToken}`
  402. }
  403. });
  404. expect(response.status).toBe(404);
  405. });
  406. });
  407. describe('GET /salary/byProvinceCity', () => {
  408. let testSalary: SalaryLevel;
  409. beforeEach(async () => {
  410. // 创建测试薪资数据
  411. const dataSource = await IntegrationTestDatabase.getDataSource();
  412. const salaryRepository = dataSource.getRepository(SalaryLevel);
  413. testSalary = salaryRepository.create({
  414. provinceId: testProvince.id,
  415. cityId: testCity.id,
  416. basicSalary: 5000.00,
  417. allowance: 1000.00,
  418. insurance: 500.00,
  419. housingFund: 800.00,
  420. totalSalary: 7300.00,
  421. });
  422. await salaryRepository.save(testSalary);
  423. });
  424. it('应该按省份城市查询薪资', async () => {
  425. const response = await client.byProvinceCity.$get({
  426. query: {
  427. provinceId: testProvince.id.toString(),
  428. cityId: testCity.id.toString()
  429. }
  430. }, {
  431. headers: {
  432. 'Authorization': `Bearer ${testToken}`
  433. }
  434. });
  435. expect(response.status).toBe(200);
  436. if (response.status === 200) {
  437. const data = await response.json();
  438. expect(data?.id).toBe(testSalary.id);
  439. expect(data?.provinceId).toBe(testProvince.id);
  440. expect(data?.cityId).toBe(testCity.id);
  441. }
  442. });
  443. it('应该处理不存在的区域组合', async () => {
  444. const response = await client.byProvinceCity.$get({
  445. query: {
  446. provinceId: '999999',
  447. cityId: '999999'
  448. }
  449. }, {
  450. headers: {
  451. 'Authorization': `Bearer ${testToken}`
  452. }
  453. });
  454. expect(response.status).toBe(404);
  455. });
  456. });
  457. describe('PUT /salary/update/:id', () => {
  458. let testSalary: SalaryLevel;
  459. beforeEach(async () => {
  460. // 创建测试薪资数据
  461. const dataSource = await IntegrationTestDatabase.getDataSource();
  462. const salaryRepository = dataSource.getRepository(SalaryLevel);
  463. testSalary = salaryRepository.create({
  464. provinceId: testProvince.id,
  465. cityId: testCity.id,
  466. basicSalary: 5000.00,
  467. allowance: 1000.00,
  468. insurance: 500.00,
  469. housingFund: 800.00,
  470. totalSalary: 7300.00,
  471. });
  472. await salaryRepository.save(testSalary);
  473. });
  474. it('应该成功更新薪资水平', async () => {
  475. const updateData = {
  476. basicSalary: 5500.00,
  477. allowance: 1200.00
  478. };
  479. const response = await client.update[':id'].$put({
  480. param: { id: testSalary.id.toString() },
  481. json: updateData
  482. }, {
  483. headers: {
  484. 'Authorization': `Bearer ${testToken}`
  485. }
  486. });
  487. expect(response.status).toBe(200);
  488. if (response.status === 200) {
  489. const data = await response.json();
  490. expect(data.id).toBe(testSalary.id);
  491. expect(data.basicSalary).toBe(5500.00);
  492. expect(data.allowance).toBe(1200.00);
  493. expect(data.totalSalary).toBe(8000.00); // 5500 + 1200 + 500 + 800
  494. }
  495. });
  496. it('应该验证更新后的区域数据', async () => {
  497. const updateData = {
  498. provinceId: 999999 // 不存在的区域ID
  499. };
  500. const response = await client.update[':id'].$put({
  501. param: { id: testSalary.id.toString() },
  502. json: updateData
  503. }, {
  504. headers: {
  505. 'Authorization': `Bearer ${testToken}`
  506. }
  507. });
  508. expect(response.status).toBe(400);
  509. });
  510. });
  511. describe('DELETE /salary/delete/:id', () => {
  512. let testSalary: SalaryLevel;
  513. beforeEach(async () => {
  514. // 创建测试薪资数据
  515. const dataSource = await IntegrationTestDatabase.getDataSource();
  516. const salaryRepository = dataSource.getRepository(SalaryLevel);
  517. testSalary = salaryRepository.create({
  518. provinceId: testProvince.id,
  519. cityId: testCity.id,
  520. basicSalary: 5000.00,
  521. allowance: 1000.00,
  522. insurance: 500.00,
  523. housingFund: 800.00,
  524. totalSalary: 7300.00,
  525. });
  526. await salaryRepository.save(testSalary);
  527. });
  528. it('应该成功删除薪资水平', async () => {
  529. const response = await client.delete[':id'].$delete({
  530. param: { id: testSalary.id.toString() }
  531. }, {
  532. headers: {
  533. 'Authorization': `Bearer ${testToken}`
  534. }
  535. });
  536. expect(response.status).toBe(200);
  537. if (response.status === 200) {
  538. const data = await response.json();
  539. expect(data).toEqual({ success: true });
  540. // 验证数据已删除
  541. const dataSource = await IntegrationTestDatabase.getDataSource();
  542. const salaryRepository = dataSource.getRepository(SalaryLevel);
  543. const deletedSalary = await salaryRepository.findOne({
  544. where: { id: testSalary.id }
  545. });
  546. expect(deletedSalary).toBeNull();
  547. }
  548. });
  549. it('应该处理不存在的薪资ID', async () => {
  550. const response = await client.delete[':id'].$delete({
  551. param: { id: '999999' }
  552. }, {
  553. headers: {
  554. 'Authorization': `Bearer ${testToken}`
  555. }
  556. });
  557. expect(response.status).toBe(404);
  558. });
  559. });
  560. describe('认证测试', () => {
  561. it('应该要求认证', async () => {
  562. const response = await client.list.$get({
  563. query: {}
  564. // 不提供Authorization header
  565. });
  566. expect(response.status).toBe(401);
  567. });
  568. it('应该验证无效token', async () => {
  569. const response = await client.list.$get({
  570. query: {}
  571. }, {
  572. headers: {
  573. 'Authorization': 'Bearer invalid_token'
  574. }
  575. });
  576. expect(response.status).toBe(401);
  577. });
  578. });
  579. });