orders.integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import { describe, it, expect, beforeEach } from 'vitest';
  2. import { testClient } from 'hono/testing';
  3. import {
  4. IntegrationTestDatabase,
  5. setupIntegrationDatabaseHooks,
  6. TestDataFactory
  7. } from '~/utils/server/integration-test-db';
  8. import { IntegrationTestAssertions } from '~/utils/server/integration-test-utils';
  9. import { ordersRoutesExport } from '@d8d/server/api';
  10. import { AuthService } from '@d8d/server/modules/auth/auth.service';
  11. import { UserService } from '@d8d/server/modules/users/user.service';
  12. import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
  13. // 设置集成测试钩子
  14. setupIntegrationDatabaseHooks()
  15. describe('用户端订单API集成测试', () => {
  16. let client: ReturnType<typeof testClient<typeof ordersRoutesExport>>['api']['v1'];
  17. let testToken: string;
  18. let testUser: any;
  19. let testRoute: any;
  20. let testPassenger: any;
  21. beforeEach(async () => {
  22. // 创建测试客户端
  23. client = testClient(ordersRoutesExport).api.v1;
  24. // 创建测试用户并生成token
  25. const dataSource = await IntegrationTestDatabase.getDataSource();
  26. const userService = new UserService(dataSource);
  27. const authService = new AuthService(userService);
  28. // 创建测试用户
  29. testUser = await TestDataFactory.createTestUser(dataSource);
  30. // 生成测试用户的token
  31. testToken = authService.generateToken(testUser);
  32. // 创建测试路线
  33. testRoute = await TestDataFactory.createTestRoute(dataSource);
  34. // 创建测试乘客
  35. testPassenger = await TestDataFactory.createTestPassenger(dataSource, {
  36. userId: testUser.id,
  37. name: '测试乘客'
  38. });
  39. });
  40. describe('订单创建测试', () => {
  41. it('应该成功创建订单', async () => {
  42. const orderData = {
  43. routeId: testRoute.id,
  44. passengerCount: 1,
  45. totalAmount: testRoute.price,
  46. passengerSnapshots: [
  47. {
  48. id: testPassenger.id,
  49. name: testPassenger.name,
  50. idType: testPassenger.idType,
  51. idNumber: testPassenger.idNumber,
  52. phone: testPassenger.phone
  53. }
  54. ],
  55. routeSnapshot: {
  56. id: testRoute.id,
  57. name: testRoute.name,
  58. pickupPoint: testRoute.pickupPoint,
  59. dropoffPoint: testRoute.dropoffPoint,
  60. departureTime: testRoute.departureTime,
  61. price: testRoute.price,
  62. vehicleType: testRoute.vehicleType,
  63. travelMode: testRoute.travelMode
  64. }
  65. };
  66. const response = await client.orders.$post({
  67. json: orderData,
  68. },
  69. {
  70. headers: {
  71. 'Authorization': `Bearer ${testToken}`
  72. }
  73. });
  74. // 断言响应
  75. expect(response.status).toBe(201);
  76. if (response.status === 201) {
  77. const responseData = await response.json();
  78. expect(responseData).toHaveProperty('id');
  79. expect(responseData.userId).toBe(testUser.id);
  80. expect(responseData.routeId).toBe(orderData.routeId);
  81. expect(responseData.passengerCount).toBe(orderData.passengerCount);
  82. expect(responseData.totalAmount).toBe(orderData.totalAmount);
  83. expect(responseData.status).toBe(OrderStatus.PENDING_PAYMENT);
  84. expect(responseData.paymentStatus).toBe(PaymentStatus.PENDING);
  85. expect(responseData.passengerSnapshots).toEqual(orderData.passengerSnapshots);
  86. // 由于parseWithAwait会将Date转换为字符串,我们需要比较字符串格式
  87. const expectedRouteSnapshot = {
  88. ...orderData.routeSnapshot,
  89. departureTime: orderData.routeSnapshot.departureTime.toISOString()
  90. };
  91. expect(responseData.routeSnapshot).toEqual(expectedRouteSnapshot);
  92. // 断言数据库中存在订单
  93. await IntegrationTestAssertions.expectOrderToExist(responseData.id);
  94. } else {
  95. // 调试信息
  96. const errorData = await response.json();
  97. console.debug('订单创建失败:', errorData);
  98. }
  99. });
  100. it('应该拒绝创建不存在的路线订单', async () => {
  101. const orderData = {
  102. routeId: 999999, // 不存在的路线ID
  103. passengerCount: 1,
  104. totalAmount: 100,
  105. passengerSnapshots: [],
  106. routeSnapshot: {
  107. id: 999999,
  108. name: '不存在的路线',
  109. pickupPoint: '起点',
  110. dropoffPoint: '终点',
  111. departureTime: new Date(),
  112. price: 100,
  113. vehicleType: 'CAR',
  114. travelMode: 'DRIVING'
  115. }
  116. };
  117. const response = await client.orders.$post({
  118. json: orderData,
  119. },
  120. {
  121. headers: {
  122. 'Authorization': `Bearer ${testToken}`
  123. }
  124. });
  125. // 应该返回404错误
  126. expect(response.status).toBe(404);
  127. if (response.status === 404) {
  128. const responseData = await response.json();
  129. expect(responseData.message).toContain('路线不存在');
  130. }
  131. });
  132. it('应该拒绝创建超过座位数的订单', async () => {
  133. const orderData = {
  134. routeId: testRoute.id,
  135. passengerCount: testRoute.availableSeats + 1, // 超过可用座位数
  136. totalAmount: testRoute.price * (testRoute.availableSeats + 1),
  137. passengerSnapshots: [],
  138. routeSnapshot: {
  139. id: testRoute.id,
  140. name: testRoute.name,
  141. pickupPoint: testRoute.pickupPoint,
  142. dropoffPoint: testRoute.dropoffPoint,
  143. departureTime: testRoute.departureTime,
  144. price: testRoute.price,
  145. vehicleType: testRoute.vehicleType,
  146. travelMode: testRoute.travelMode
  147. }
  148. };
  149. const response = await client.orders.$post({
  150. json: orderData,
  151. },
  152. {
  153. headers: {
  154. 'Authorization': `Bearer ${testToken}`
  155. }
  156. });
  157. // 应该返回422业务逻辑错误
  158. expect(response.status).toBe(422);
  159. if (response.status === 422) {
  160. const responseData = await response.json();
  161. expect(responseData.message).toContain('乘客数量超过路线座位数');
  162. }
  163. });
  164. it('应该验证乘客快照信息', async () => {
  165. const orderData = {
  166. routeId: testRoute.id,
  167. passengerCount: 1,
  168. totalAmount: testRoute.price,
  169. passengerSnapshots: [
  170. {
  171. id: 999999, // 不存在的乘客ID
  172. name: '不存在的乘客',
  173. idType: 'ID_CARD',
  174. idNumber: '110101199001011234',
  175. phone: '13812345678'
  176. }
  177. ],
  178. routeSnapshot: {
  179. id: testRoute.id,
  180. name: testRoute.name,
  181. pickupPoint: testRoute.pickupPoint,
  182. dropoffPoint: testRoute.dropoffPoint,
  183. departureTime: testRoute.departureTime,
  184. price: testRoute.price,
  185. vehicleType: testRoute.vehicleType,
  186. travelMode: testRoute.travelMode
  187. }
  188. };
  189. const response = await client.orders.$post({
  190. json: orderData,
  191. },
  192. {
  193. headers: {
  194. 'Authorization': `Bearer ${testToken}`
  195. }
  196. });
  197. // 应该返回404错误
  198. expect(response.status).toBe(404);
  199. if (response.status === 404) {
  200. const responseData = await response.json();
  201. expect(responseData.message).toContain('乘客ID 999999 不存在');
  202. }
  203. });
  204. it('应该拒绝创建缺少必填字段的订单', async () => {
  205. const orderData = {
  206. // 缺少routeId
  207. routeId: undefined as any, // 故意不提供routeId来测试验证
  208. passengerCount: 1,
  209. totalAmount: 100,
  210. passengerSnapshots: [],
  211. routeSnapshot: {
  212. id: testRoute.id,
  213. name: testRoute.name,
  214. pickupPoint: testRoute.pickupPoint,
  215. dropoffPoint: testRoute.dropoffPoint,
  216. departureTime: testRoute.departureTime,
  217. price: testRoute.price,
  218. vehicleType: testRoute.vehicleType,
  219. travelMode: testRoute.travelMode
  220. }
  221. };
  222. const response = await client.orders.$post({
  223. json: orderData,
  224. },
  225. {
  226. headers: {
  227. 'Authorization': `Bearer ${testToken}`
  228. }
  229. });
  230. // 应该返回验证错误
  231. expect([400, 500]).toContain(response.status);
  232. });
  233. it('应该正确计算订单金额', async () => {
  234. const passengerCount = 2;
  235. const expectedTotalAmount = testRoute.price * passengerCount;
  236. const orderData = {
  237. routeId: testRoute.id,
  238. passengerCount: passengerCount,
  239. totalAmount: expectedTotalAmount,
  240. passengerSnapshots: [
  241. {
  242. id: testPassenger.id,
  243. name: testPassenger.name,
  244. idType: testPassenger.idType,
  245. idNumber: testPassenger.idNumber,
  246. phone: testPassenger.phone
  247. },
  248. {
  249. name: '第二个乘客',
  250. idType: 'ID_CARD',
  251. idNumber: '110101199001012345',
  252. phone: '13987654321'
  253. }
  254. ],
  255. routeSnapshot: {
  256. id: testRoute.id,
  257. name: testRoute.name,
  258. pickupPoint: testRoute.pickupPoint,
  259. dropoffPoint: testRoute.dropoffPoint,
  260. departureTime: testRoute.departureTime,
  261. price: testRoute.price,
  262. vehicleType: testRoute.vehicleType,
  263. travelMode: testRoute.travelMode
  264. }
  265. };
  266. const response = await client.orders.$post({
  267. json: orderData,
  268. },
  269. {
  270. headers: {
  271. 'Authorization': `Bearer ${testToken}`
  272. }
  273. });
  274. expect(response.status).toBe(201);
  275. if (response.status === 201) {
  276. const responseData = await response.json();
  277. expect(responseData.totalAmount).toBe(expectedTotalAmount);
  278. expect(responseData.passengerCount).toBe(passengerCount);
  279. }
  280. });
  281. });
  282. describe('权限控制测试', () => {
  283. it('应该拒绝未认证用户的订单创建', async () => {
  284. const orderData = {
  285. routeId: testRoute.id,
  286. passengerCount: 1,
  287. totalAmount: testRoute.price,
  288. passengerSnapshots: [],
  289. routeSnapshot: {
  290. id: testRoute.id,
  291. name: testRoute.name,
  292. pickupPoint: testRoute.pickupPoint,
  293. dropoffPoint: testRoute.dropoffPoint,
  294. departureTime: testRoute.departureTime,
  295. price: testRoute.price,
  296. vehicleType: testRoute.vehicleType,
  297. travelMode: testRoute.travelMode
  298. }
  299. };
  300. const response = await client.orders.$post({
  301. json: orderData,
  302. });
  303. expect(response.status).toBe(401);
  304. });
  305. it('应该拒绝无效token的订单创建', async () => {
  306. const orderData = {
  307. routeId: testRoute.id,
  308. passengerCount: 1,
  309. totalAmount: testRoute.price,
  310. passengerSnapshots: [],
  311. routeSnapshot: {
  312. id: testRoute.id,
  313. name: testRoute.name,
  314. pickupPoint: testRoute.pickupPoint,
  315. dropoffPoint: testRoute.dropoffPoint,
  316. departureTime: testRoute.departureTime,
  317. price: testRoute.price,
  318. vehicleType: testRoute.vehicleType,
  319. travelMode: testRoute.travelMode
  320. }
  321. };
  322. const response = await client.orders.$post({
  323. json: orderData,
  324. },
  325. {
  326. headers: {
  327. 'Authorization': 'Bearer invalid_token'
  328. }
  329. });
  330. expect(response.status).toBe(401);
  331. });
  332. });
  333. describe('快照机制测试', () => {
  334. it('应该正确保存路线快照信息', async () => {
  335. const routeSnapshot = {
  336. id: testRoute.id,
  337. name: testRoute.name,
  338. pickupPoint: testRoute.pickupPoint,
  339. dropoffPoint: testRoute.dropoffPoint,
  340. departureTime: testRoute.departureTime,
  341. price: testRoute.price,
  342. vehicleType: testRoute.vehicleType,
  343. travelMode: testRoute.travelMode
  344. };
  345. const orderData = {
  346. routeId: testRoute.id,
  347. passengerCount: 1,
  348. totalAmount: testRoute.price,
  349. passengerSnapshots: [],
  350. routeSnapshot: routeSnapshot
  351. };
  352. const response = await client.orders.$post({
  353. json: orderData,
  354. },
  355. {
  356. headers: {
  357. 'Authorization': `Bearer ${testToken}`
  358. }
  359. });
  360. expect(response.status).toBe(201);
  361. if (response.status === 201) {
  362. const responseData = await response.json();
  363. // 由于parseWithAwait会将Date转换为字符串,我们需要比较字符串格式
  364. const expectedRouteSnapshot = {
  365. ...routeSnapshot,
  366. departureTime: routeSnapshot.departureTime.toISOString()
  367. };
  368. expect(responseData.routeSnapshot).toEqual(expectedRouteSnapshot);
  369. // 验证数据库中的快照数据
  370. const order = await IntegrationTestAssertions.getOrderById(responseData.id);
  371. expect(order).not.toBeNull();
  372. // 数据库中的routeSnapshot应该包含字符串格式的时间
  373. const expectedDbRouteSnapshot = {
  374. ...routeSnapshot,
  375. departureTime: routeSnapshot.departureTime.toISOString()
  376. };
  377. expect(order!.routeSnapshot).toEqual(expectedDbRouteSnapshot);
  378. }
  379. });
  380. it('应该正确保存乘客快照信息', async () => {
  381. const passengerSnapshots = [
  382. {
  383. id: testPassenger.id,
  384. name: testPassenger.name,
  385. idType: testPassenger.idType,
  386. idNumber: testPassenger.idNumber,
  387. phone: testPassenger.phone,
  388. isDefault: testPassenger.isDefault
  389. },
  390. {
  391. name: '新乘客',
  392. idType: 'PASSPORT',
  393. idNumber: 'E12345678',
  394. phone: '13987654321',
  395. isDefault: false
  396. }
  397. ];
  398. const orderData = {
  399. routeId: testRoute.id,
  400. passengerCount: passengerSnapshots.length,
  401. totalAmount: testRoute.price * passengerSnapshots.length,
  402. passengerSnapshots: passengerSnapshots,
  403. routeSnapshot: {
  404. id: testRoute.id,
  405. name: testRoute.name,
  406. pickupPoint: testRoute.pickupPoint,
  407. dropoffPoint: testRoute.dropoffPoint,
  408. departureTime: testRoute.departureTime,
  409. price: testRoute.price,
  410. vehicleType: testRoute.vehicleType,
  411. travelMode: testRoute.travelMode
  412. }
  413. };
  414. const response = await client.orders.$post({
  415. json: orderData,
  416. },
  417. {
  418. headers: {
  419. 'Authorization': `Bearer ${testToken}`
  420. }
  421. });
  422. expect(response.status).toBe(201);
  423. if (response.status === 201) {
  424. const responseData = await response.json();
  425. expect(responseData.passengerSnapshots).toEqual(passengerSnapshots);
  426. // 验证数据库中的快照数据
  427. const order = await IntegrationTestAssertions.getOrderById(responseData.id);
  428. expect(order).not.toBeNull();
  429. expect(order!.passengerSnapshots).toEqual(passengerSnapshots);
  430. }
  431. });
  432. });
  433. describe('订单状态测试', () => {
  434. it('新创建的订单应该处于待支付状态', async () => {
  435. const orderData = {
  436. routeId: testRoute.id,
  437. passengerCount: 1,
  438. totalAmount: testRoute.price,
  439. passengerSnapshots: [],
  440. routeSnapshot: {
  441. id: testRoute.id,
  442. name: testRoute.name,
  443. pickupPoint: testRoute.pickupPoint,
  444. dropoffPoint: testRoute.dropoffPoint,
  445. departureTime: testRoute.departureTime,
  446. price: testRoute.price,
  447. vehicleType: testRoute.vehicleType,
  448. travelMode: testRoute.travelMode
  449. }
  450. };
  451. const response = await client.orders.$post({
  452. json: orderData,
  453. },
  454. {
  455. headers: {
  456. 'Authorization': `Bearer ${testToken}`
  457. }
  458. });
  459. expect(response.status).toBe(201);
  460. if (response.status === 201) {
  461. const responseData = await response.json();
  462. expect(responseData.status).toBe(OrderStatus.PENDING_PAYMENT);
  463. expect(responseData.paymentStatus).toBe(PaymentStatus.PENDING);
  464. }
  465. });
  466. });
  467. });