salary.integration.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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. // 第一次创建
  133. const createData = {
  134. provinceId: testProvince.id,
  135. cityId: testCity.id,
  136. basicSalary: 5000.00
  137. };
  138. const firstResponse = await client.create.$post({
  139. json: createData
  140. }, {
  141. headers: {
  142. 'Authorization': `Bearer ${testToken}`
  143. }
  144. });
  145. expect(firstResponse.status).toBe(200);
  146. // 第二次创建相同区域
  147. const secondResponse = await client.create.$post({
  148. json: createData
  149. }, {
  150. headers: {
  151. 'Authorization': `Bearer ${testToken}`
  152. }
  153. });
  154. expect(secondResponse.status).toBe(400);
  155. });
  156. });
  157. describe('GET /salary/list', () => {
  158. beforeEach(async () => {
  159. // 创建测试数据
  160. const dataSource = await IntegrationTestDatabase.getDataSource();
  161. const salaryRepository = dataSource.getRepository(SalaryLevel);
  162. const salary1 = salaryRepository.create({
  163. provinceId: testProvince.id,
  164. cityId: testCity.id,
  165. districtId: testDistrict.id,
  166. basicSalary: 5000.00,
  167. allowance: 1000.00,
  168. insurance: 500.00,
  169. housingFund: 800.00,
  170. totalSalary: 7300.00
  171. });
  172. await salaryRepository.save(salary1);
  173. const salary2 = salaryRepository.create({
  174. provinceId: testProvince2.id, // 使用不同的省份
  175. cityId: testCity2.id, // 使用不同的城市
  176. basicSalary: 6000.00,
  177. allowance: 1200.00,
  178. insurance: 600.00,
  179. housingFund: 900.00,
  180. totalSalary: 8700.00
  181. });
  182. await salaryRepository.save(salary2);
  183. });
  184. it('应该返回所有薪资水平列表', async () => {
  185. const response = await client.list.$get({
  186. query: {}
  187. }, {
  188. headers: {
  189. 'Authorization': `Bearer ${testToken}`
  190. }
  191. });
  192. expect(response.status).toBe(200);
  193. if (response.status === 200) {
  194. const data = await response.json();
  195. expect(data).toHaveProperty('data');
  196. expect(data).toHaveProperty('total');
  197. expect(data.data).toHaveLength(2);
  198. expect(data.total).toBe(2);
  199. }
  200. });
  201. it('应该支持按区域ID过滤', async () => {
  202. const response = await client.list.$get({
  203. query: {
  204. provinceId: testProvince.id.toString(),
  205. cityId: testCity.id.toString()
  206. }
  207. }, {
  208. headers: {
  209. 'Authorization': `Bearer ${testToken}`
  210. }
  211. });
  212. expect(response.status).toBe(200);
  213. if (response.status === 200) {
  214. const data = await response.json();
  215. expect(data.data).toHaveLength(1);
  216. }
  217. });
  218. it('应该支持分页查询', async () => {
  219. const response = await client.list.$get({
  220. query: {
  221. skip: '0',
  222. take: '1'
  223. }
  224. }, {
  225. headers: {
  226. 'Authorization': `Bearer ${testToken}`
  227. }
  228. });
  229. expect(response.status).toBe(200);
  230. if (response.status === 200) {
  231. const data = await response.json();
  232. expect(data.data).toHaveLength(1);
  233. expect(data.total).toBe(2);
  234. }
  235. });
  236. });
  237. describe('GET /salary/detail/:id', () => {
  238. let testSalary: SalaryLevel;
  239. beforeEach(async () => {
  240. // 创建测试薪资数据
  241. const dataSource = await IntegrationTestDatabase.getDataSource();
  242. const salaryRepository = dataSource.getRepository(SalaryLevel);
  243. testSalary = salaryRepository.create({
  244. provinceId: testProvince.id,
  245. cityId: testCity.id,
  246. districtId: testDistrict.id,
  247. basicSalary: 5000.00,
  248. allowance: 1000.00,
  249. insurance: 500.00,
  250. housingFund: 800.00,
  251. totalSalary: 7300.00,
  252. });
  253. await salaryRepository.save(testSalary);
  254. });
  255. it('应该返回指定ID的薪资详情', async () => {
  256. const response = await client.detail[':id'].$get({
  257. param: { id: testSalary.id.toString() }
  258. }, {
  259. headers: {
  260. 'Authorization': `Bearer ${testToken}`
  261. }
  262. });
  263. expect(response.status).toBe(200);
  264. if (response.status === 200) {
  265. const data = await response.json();
  266. expect(data?.id).toBe(testSalary.id);
  267. expect(data?.provinceId).toBe(testProvince.id);
  268. expect(data?.cityId).toBe(testCity.id);
  269. }
  270. });
  271. it('应该处理不存在的薪资ID', async () => {
  272. const response = await client.detail[':id'].$get({
  273. param: { id: '999999' }
  274. }, {
  275. headers: {
  276. 'Authorization': `Bearer ${testToken}`
  277. }
  278. });
  279. expect(response.status).toBe(404);
  280. });
  281. });
  282. describe('GET /salary/byProvinceCity', () => {
  283. let testSalary: SalaryLevel;
  284. beforeEach(async () => {
  285. // 创建测试薪资数据
  286. const dataSource = await IntegrationTestDatabase.getDataSource();
  287. const salaryRepository = dataSource.getRepository(SalaryLevel);
  288. testSalary = salaryRepository.create({
  289. provinceId: testProvince.id,
  290. cityId: testCity.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(testSalary);
  298. });
  299. it('应该按省份城市查询薪资', async () => {
  300. const response = await client.byProvinceCity.$get({
  301. query: {
  302. provinceId: testProvince.id.toString(),
  303. cityId: testCity.id.toString()
  304. }
  305. }, {
  306. headers: {
  307. 'Authorization': `Bearer ${testToken}`
  308. }
  309. });
  310. expect(response.status).toBe(200);
  311. if (response.status === 200) {
  312. const data = await response.json();
  313. expect(data?.id).toBe(testSalary.id);
  314. expect(data?.provinceId).toBe(testProvince.id);
  315. expect(data?.cityId).toBe(testCity.id);
  316. }
  317. });
  318. it('应该处理不存在的区域组合', async () => {
  319. const response = await client.byProvinceCity.$get({
  320. query: {
  321. provinceId: '999999',
  322. cityId: '999999'
  323. }
  324. }, {
  325. headers: {
  326. 'Authorization': `Bearer ${testToken}`
  327. }
  328. });
  329. expect(response.status).toBe(404);
  330. });
  331. });
  332. describe('PUT /salary/update/:id', () => {
  333. let testSalary: SalaryLevel;
  334. beforeEach(async () => {
  335. // 创建测试薪资数据
  336. const dataSource = await IntegrationTestDatabase.getDataSource();
  337. const salaryRepository = dataSource.getRepository(SalaryLevel);
  338. testSalary = salaryRepository.create({
  339. provinceId: testProvince.id,
  340. cityId: testCity.id,
  341. basicSalary: 5000.00,
  342. allowance: 1000.00,
  343. insurance: 500.00,
  344. housingFund: 800.00,
  345. totalSalary: 7300.00,
  346. });
  347. await salaryRepository.save(testSalary);
  348. });
  349. it('应该成功更新薪资水平', async () => {
  350. const updateData = {
  351. basicSalary: 5500.00,
  352. allowance: 1200.00
  353. };
  354. const response = await client.update[':id'].$put({
  355. param: { id: testSalary.id.toString() },
  356. json: updateData
  357. }, {
  358. headers: {
  359. 'Authorization': `Bearer ${testToken}`
  360. }
  361. });
  362. expect(response.status).toBe(200);
  363. if (response.status === 200) {
  364. const data = await response.json();
  365. expect(data.id).toBe(testSalary.id);
  366. expect(data.basicSalary).toBe(5500.00);
  367. expect(data.allowance).toBe(1200.00);
  368. expect(data.totalSalary).toBe(8000.00); // 5500 + 1200 + 500 + 800
  369. }
  370. });
  371. it('应该验证更新后的区域数据', async () => {
  372. const updateData = {
  373. provinceId: 999999 // 不存在的区域ID
  374. };
  375. const response = await client.update[':id'].$put({
  376. param: { id: testSalary.id.toString() },
  377. json: updateData
  378. }, {
  379. headers: {
  380. 'Authorization': `Bearer ${testToken}`
  381. }
  382. });
  383. expect(response.status).toBe(400);
  384. });
  385. });
  386. describe('DELETE /salary/delete/:id', () => {
  387. let testSalary: SalaryLevel;
  388. beforeEach(async () => {
  389. // 创建测试薪资数据
  390. const dataSource = await IntegrationTestDatabase.getDataSource();
  391. const salaryRepository = dataSource.getRepository(SalaryLevel);
  392. testSalary = salaryRepository.create({
  393. provinceId: testProvince.id,
  394. cityId: testCity.id,
  395. basicSalary: 5000.00,
  396. allowance: 1000.00,
  397. insurance: 500.00,
  398. housingFund: 800.00,
  399. totalSalary: 7300.00,
  400. });
  401. await salaryRepository.save(testSalary);
  402. });
  403. it('应该成功删除薪资水平', async () => {
  404. const response = await client.delete[':id'].$delete({
  405. param: { id: testSalary.id.toString() }
  406. }, {
  407. headers: {
  408. 'Authorization': `Bearer ${testToken}`
  409. }
  410. });
  411. expect(response.status).toBe(200);
  412. if (response.status === 200) {
  413. const data = await response.json();
  414. expect(data).toEqual({ success: true });
  415. // 验证数据已删除
  416. const dataSource = await IntegrationTestDatabase.getDataSource();
  417. const salaryRepository = dataSource.getRepository(SalaryLevel);
  418. const deletedSalary = await salaryRepository.findOne({
  419. where: { id: testSalary.id }
  420. });
  421. expect(deletedSalary).toBeNull();
  422. }
  423. });
  424. it('应该处理不存在的薪资ID', async () => {
  425. const response = await client.delete[':id'].$delete({
  426. param: { id: '999999' }
  427. }, {
  428. headers: {
  429. 'Authorization': `Bearer ${testToken}`
  430. }
  431. });
  432. expect(response.status).toBe(404);
  433. });
  434. });
  435. describe('认证测试', () => {
  436. it('应该要求认证', async () => {
  437. const response = await client.list.$get({
  438. query: {}
  439. // 不提供Authorization header
  440. });
  441. expect(response.status).toBe(401);
  442. });
  443. it('应该验证无效token', async () => {
  444. const response = await client.list.$get({
  445. query: {}
  446. }, {
  447. headers: {
  448. 'Authorization': 'Bearer invalid_token'
  449. }
  450. });
  451. expect(response.status).toBe(401);
  452. });
  453. });
  454. });