orders.integration.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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. };
  108. const response = await client.orders.$post({
  109. json: orderData,
  110. },
  111. {
  112. headers: {
  113. 'Authorization': `Bearer ${testToken}`
  114. }
  115. });
  116. // 应该返回404错误
  117. expect(response.status).toBe(404);
  118. if (response.status === 404) {
  119. const responseData = await response.json();
  120. expect(responseData.message).toContain('路线不存在');
  121. }
  122. });
  123. it('应该拒绝创建超过座位数的订单', async () => {
  124. const orderData = {
  125. routeId: testRoute.id,
  126. passengerCount: testRoute.availableSeats + 1, // 超过可用座位数
  127. totalAmount: testRoute.price * (testRoute.availableSeats + 1),
  128. passengerSnapshots: [],
  129. routeSnapshot: {}
  130. };
  131. const response = await client.orders.$post({
  132. json: orderData,
  133. },
  134. {
  135. headers: {
  136. 'Authorization': `Bearer ${testToken}`
  137. }
  138. });
  139. // 应该返回422业务逻辑错误
  140. expect(response.status).toBe(422);
  141. if (response.status === 422) {
  142. const responseData = await response.json();
  143. expect(responseData.message).toContain('乘客数量超过路线座位数');
  144. }
  145. });
  146. it('应该验证乘客快照信息', async () => {
  147. const orderData = {
  148. routeId: testRoute.id,
  149. passengerCount: 1,
  150. totalAmount: testRoute.price,
  151. passengerSnapshots: [
  152. {
  153. id: 999999, // 不存在的乘客ID
  154. name: '不存在的乘客',
  155. idType: 'ID_CARD',
  156. idNumber: '110101199001011234',
  157. phone: '13812345678'
  158. }
  159. ],
  160. routeSnapshot: {}
  161. };
  162. const response = await client.orders.$post({
  163. json: orderData,
  164. },
  165. {
  166. headers: {
  167. 'Authorization': `Bearer ${testToken}`
  168. }
  169. });
  170. // 应该返回404错误
  171. expect(response.status).toBe(404);
  172. if (response.status === 404) {
  173. const responseData = await response.json();
  174. expect(responseData.message).toContain('乘客ID 999999 不存在');
  175. }
  176. });
  177. it('应该拒绝创建缺少必填字段的订单', async () => {
  178. const orderData = {
  179. // 缺少routeId
  180. routeId: undefined as any, // 故意不提供routeId来测试验证
  181. passengerCount: 1,
  182. totalAmount: 100,
  183. passengerSnapshots: [],
  184. routeSnapshot: {}
  185. };
  186. const response = await client.orders.$post({
  187. json: orderData,
  188. },
  189. {
  190. headers: {
  191. 'Authorization': `Bearer ${testToken}`
  192. }
  193. });
  194. // 应该返回验证错误
  195. expect([400, 500]).toContain(response.status);
  196. });
  197. it('应该正确计算订单金额', async () => {
  198. const passengerCount = 2;
  199. const expectedTotalAmount = testRoute.price * passengerCount;
  200. const orderData = {
  201. routeId: testRoute.id,
  202. passengerCount: passengerCount,
  203. totalAmount: expectedTotalAmount,
  204. passengerSnapshots: [
  205. {
  206. id: testPassenger.id,
  207. name: testPassenger.name,
  208. idType: testPassenger.idType,
  209. idNumber: testPassenger.idNumber,
  210. phone: testPassenger.phone
  211. },
  212. {
  213. name: '第二个乘客',
  214. idType: 'ID_CARD',
  215. idNumber: '110101199001012345',
  216. phone: '13987654321'
  217. }
  218. ],
  219. routeSnapshot: {
  220. id: testRoute.id,
  221. name: testRoute.name,
  222. pickupPoint: testRoute.pickupPoint,
  223. dropoffPoint: testRoute.dropoffPoint,
  224. departureTime: testRoute.departureTime,
  225. price: testRoute.price,
  226. vehicleType: testRoute.vehicleType,
  227. travelMode: testRoute.travelMode
  228. }
  229. };
  230. const response = await client.orders.$post({
  231. json: orderData,
  232. },
  233. {
  234. headers: {
  235. 'Authorization': `Bearer ${testToken}`
  236. }
  237. });
  238. expect(response.status).toBe(201);
  239. if (response.status === 201) {
  240. const responseData = await response.json();
  241. expect(responseData.totalAmount).toBe(expectedTotalAmount);
  242. expect(responseData.passengerCount).toBe(passengerCount);
  243. }
  244. });
  245. });
  246. describe('权限控制测试', () => {
  247. it('应该拒绝未认证用户的订单创建', async () => {
  248. const orderData = {
  249. routeId: testRoute.id,
  250. passengerCount: 1,
  251. totalAmount: testRoute.price,
  252. passengerSnapshots: [],
  253. routeSnapshot: {}
  254. };
  255. const response = await client.orders.$post({
  256. json: orderData,
  257. });
  258. expect(response.status).toBe(401);
  259. });
  260. it('应该拒绝无效token的订单创建', async () => {
  261. const orderData = {
  262. routeId: testRoute.id,
  263. passengerCount: 1,
  264. totalAmount: testRoute.price,
  265. passengerSnapshots: [],
  266. routeSnapshot: {}
  267. };
  268. const response = await client.orders.$post({
  269. json: orderData,
  270. },
  271. {
  272. headers: {
  273. 'Authorization': 'Bearer invalid_token'
  274. }
  275. });
  276. expect(response.status).toBe(401);
  277. });
  278. });
  279. describe('快照机制测试', () => {
  280. it('应该正确保存路线快照信息', async () => {
  281. const routeSnapshot = {
  282. id: testRoute.id,
  283. name: testRoute.name,
  284. pickupPoint: testRoute.pickupPoint,
  285. dropoffPoint: testRoute.dropoffPoint,
  286. departureTime: testRoute.departureTime,
  287. price: testRoute.price,
  288. vehicleType: testRoute.vehicleType,
  289. travelMode: testRoute.travelMode
  290. };
  291. const orderData = {
  292. routeId: testRoute.id,
  293. passengerCount: 1,
  294. totalAmount: testRoute.price,
  295. passengerSnapshots: [],
  296. routeSnapshot: routeSnapshot
  297. };
  298. const response = await client.orders.$post({
  299. json: orderData,
  300. },
  301. {
  302. headers: {
  303. 'Authorization': `Bearer ${testToken}`
  304. }
  305. });
  306. expect(response.status).toBe(201);
  307. if (response.status === 201) {
  308. const responseData = await response.json();
  309. // 由于parseWithAwait会将Date转换为字符串,我们需要比较字符串格式
  310. const expectedRouteSnapshot = {
  311. ...routeSnapshot,
  312. departureTime: routeSnapshot.departureTime.toISOString()
  313. };
  314. expect(responseData.routeSnapshot).toEqual(expectedRouteSnapshot);
  315. // 验证数据库中的快照数据
  316. const order = await IntegrationTestAssertions.getOrderById(responseData.id);
  317. expect(order).not.toBeNull();
  318. // 数据库中的routeSnapshot应该包含字符串格式的时间
  319. const expectedDbRouteSnapshot = {
  320. ...routeSnapshot,
  321. departureTime: routeSnapshot.departureTime.toISOString()
  322. };
  323. expect(order!.routeSnapshot).toEqual(expectedDbRouteSnapshot);
  324. }
  325. });
  326. it('应该正确保存乘客快照信息', async () => {
  327. const passengerSnapshots = [
  328. {
  329. id: testPassenger.id,
  330. name: testPassenger.name,
  331. idType: testPassenger.idType,
  332. idNumber: testPassenger.idNumber,
  333. phone: testPassenger.phone,
  334. isDefault: testPassenger.isDefault
  335. },
  336. {
  337. name: '新乘客',
  338. idType: 'PASSPORT',
  339. idNumber: 'E12345678',
  340. phone: '13987654321',
  341. isDefault: false
  342. }
  343. ];
  344. const orderData = {
  345. routeId: testRoute.id,
  346. passengerCount: passengerSnapshots.length,
  347. totalAmount: testRoute.price * passengerSnapshots.length,
  348. passengerSnapshots: passengerSnapshots,
  349. routeSnapshot: {
  350. id: testRoute.id,
  351. name: testRoute.name,
  352. pickupPoint: testRoute.pickupPoint,
  353. dropoffPoint: testRoute.dropoffPoint,
  354. departureTime: testRoute.departureTime,
  355. price: testRoute.price,
  356. vehicleType: testRoute.vehicleType,
  357. travelMode: testRoute.travelMode
  358. }
  359. };
  360. const response = await client.orders.$post({
  361. json: orderData,
  362. },
  363. {
  364. headers: {
  365. 'Authorization': `Bearer ${testToken}`
  366. }
  367. });
  368. expect(response.status).toBe(201);
  369. if (response.status === 201) {
  370. const responseData = await response.json();
  371. expect(responseData.passengerSnapshots).toEqual(passengerSnapshots);
  372. // 验证数据库中的快照数据
  373. const order = await IntegrationTestAssertions.getOrderById(responseData.id);
  374. expect(order).not.toBeNull();
  375. expect(order!.passengerSnapshots).toEqual(passengerSnapshots);
  376. }
  377. });
  378. });
  379. describe('订单状态测试', () => {
  380. it('新创建的订单应该处于待支付状态', async () => {
  381. const orderData = {
  382. routeId: testRoute.id,
  383. passengerCount: 1,
  384. totalAmount: testRoute.price,
  385. passengerSnapshots: [],
  386. routeSnapshot: {
  387. id: testRoute.id,
  388. name: testRoute.name,
  389. pickupPoint: testRoute.pickupPoint,
  390. dropoffPoint: testRoute.dropoffPoint,
  391. departureTime: testRoute.departureTime,
  392. price: testRoute.price,
  393. vehicleType: testRoute.vehicleType,
  394. travelMode: testRoute.travelMode
  395. }
  396. };
  397. const response = await client.orders.$post({
  398. json: orderData,
  399. },
  400. {
  401. headers: {
  402. 'Authorization': `Bearer ${testToken}`
  403. }
  404. });
  405. expect(response.status).toBe(201);
  406. if (response.status === 201) {
  407. const responseData = await response.json();
  408. expect(responseData.status).toBe(OrderStatus.PENDING_PAYMENT);
  409. expect(responseData.paymentStatus).toBe(PaymentStatus.PENDING);
  410. }
  411. });
  412. });
  413. });