search.integration.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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 { routesRoutesExport } from '@/server/api';
  10. import { VehicleType, TravelMode } from '@/server/modules/routes/route.schema';
  11. // 设置集成测试钩子
  12. setupIntegrationDatabaseHooks()
  13. describe('用户端路线搜索API集成测试', () => {
  14. let client: ReturnType<typeof testClient<typeof routesRoutesExport>>['api']['v1'];
  15. beforeEach(async () => {
  16. // 创建测试客户端
  17. client = testClient(routesRoutesExport).api.v1;
  18. // 创建测试数据
  19. const dataSource = await IntegrationTestDatabase.getDataSource();
  20. if (!dataSource) throw new Error('Database not initialized');
  21. // 创建测试活动
  22. const activity1 = await TestDataFactory.createTestActivity(dataSource, {
  23. name: '测试活动1'
  24. });
  25. const activity2 = await TestDataFactory.createTestActivity(dataSource, {
  26. name: '测试活动2'
  27. });
  28. // 创建测试路线,覆盖所有组合查询场景
  29. // 大巴拼车路线
  30. await TestDataFactory.createTestRoute(dataSource, {
  31. name: '大巴拼车路线1',
  32. vehicleType: VehicleType.BUS,
  33. travelMode: TravelMode.CARPOOL,
  34. activityId: activity1.id,
  35. price: 100,
  36. availableSeats: 30
  37. });
  38. await TestDataFactory.createTestRoute(dataSource, {
  39. name: '大巴拼车路线2',
  40. vehicleType: VehicleType.BUS,
  41. travelMode: TravelMode.CARPOOL,
  42. activityId: activity2.id,
  43. price: 120,
  44. availableSeats: 25
  45. });
  46. // 商务车路线(拼车)
  47. await TestDataFactory.createTestRoute(dataSource, {
  48. name: '商务车拼车路线',
  49. vehicleType: VehicleType.BUSINESS,
  50. travelMode: TravelMode.CARPOOL,
  51. activityId: activity1.id,
  52. price: 200,
  53. availableSeats: 4
  54. });
  55. // 商务车路线(包车)
  56. await TestDataFactory.createTestRoute(dataSource, {
  57. name: '商务车包车路线',
  58. vehicleType: VehicleType.BUSINESS,
  59. travelMode: TravelMode.CHARTER,
  60. activityId: activity2.id,
  61. price: 500,
  62. availableSeats: 4
  63. });
  64. // 大巴包车路线
  65. await TestDataFactory.createTestRoute(dataSource, {
  66. name: '大巴包车路线',
  67. vehicleType: VehicleType.BUS,
  68. travelMode: TravelMode.CHARTER,
  69. activityId: activity1.id,
  70. price: 800,
  71. availableSeats: 40
  72. });
  73. // 中巴拼车路线(用于验证排除逻辑)
  74. await TestDataFactory.createTestRoute(dataSource, {
  75. name: '中巴拼车路线',
  76. vehicleType: VehicleType.MINIBUS,
  77. travelMode: TravelMode.CARPOOL,
  78. activityId: activity1.id,
  79. price: 150,
  80. availableSeats: 15
  81. });
  82. // 小车包车路线(用于验证排除逻辑)
  83. await TestDataFactory.createTestRoute(dataSource, {
  84. name: '小车包车路线',
  85. vehicleType: VehicleType.CAR,
  86. travelMode: TravelMode.CHARTER,
  87. activityId: activity2.id,
  88. price: 300,
  89. availableSeats: 3
  90. });
  91. });
  92. describe('组合查询逻辑测试', () => {
  93. it('应该正确查询大巴拼车组合', async () => {
  94. const response = await client.routes.search.$get({
  95. query: {
  96. vehicleType: 'bus',
  97. travelMode: 'carpool'
  98. }
  99. });
  100. IntegrationTestAssertions.expectStatus(response, 200);
  101. if (response.status === 200) {
  102. const responseData = await response.json();
  103. expect(responseData.success).toBe(true);
  104. expect(responseData.data.routes).toHaveLength(2);
  105. // 验证所有返回路线都是大巴拼车
  106. responseData.data.routes.forEach((route: any) => {
  107. expect(route.vehicleType).toBe(VehicleType.BUS);
  108. expect(route.travelMode).toBe(TravelMode.CARPOOL);
  109. });
  110. // 验证包含正确的路线
  111. const routeNames = responseData.data.routes.map((route: any) => route.name);
  112. expect(routeNames).toContain('大巴拼车路线1');
  113. expect(routeNames).toContain('大巴拼车路线2');
  114. // 验证不包含其他类型的路线
  115. expect(routeNames).not.toContain('商务车拼车路线');
  116. expect(routeNames).not.toContain('大巴包车路线');
  117. }
  118. });
  119. it('应该正确查询商务车组合(支持拼车和包车)', async () => {
  120. const response = await client.routes.search.$get({
  121. query: {
  122. vehicleType: 'business',
  123. travelMode: 'carpool,charter'
  124. }
  125. });
  126. IntegrationTestAssertions.expectStatus(response, 200);
  127. if (response.status === 200) {
  128. const responseData = await response.json();
  129. expect(responseData.success).toBe(true);
  130. expect(responseData.data.routes).toHaveLength(2);
  131. // 验证所有返回路线都是商务车
  132. responseData.data.routes.forEach((route: any) => {
  133. expect(route.vehicleType).toBe(VehicleType.BUSINESS);
  134. });
  135. // 验证包含商务车拼车和包车路线
  136. const routeNames = responseData.data.routes.map((route: any) => route.name);
  137. expect(routeNames).toContain('商务车拼车路线');
  138. expect(routeNames).toContain('商务车包车路线');
  139. // 验证不包含其他车型的路线
  140. expect(routeNames).not.toContain('大巴拼车路线1');
  141. expect(routeNames).not.toContain('中巴拼车路线');
  142. }
  143. });
  144. it('应该正确查询包车组合(支持大巴和商务车)', async () => {
  145. const response = await client.routes.search.$get({
  146. query: {
  147. vehicleType: 'bus,business',
  148. travelMode: 'charter'
  149. }
  150. });
  151. IntegrationTestAssertions.expectStatus(response, 200);
  152. if (response.status === 200) {
  153. const responseData = await response.json();
  154. expect(responseData.success).toBe(true);
  155. expect(responseData.data.routes).toHaveLength(2);
  156. // 验证所有返回路线都是包车
  157. responseData.data.routes.forEach((route: any) => {
  158. expect(route.travelMode).toBe(TravelMode.CHARTER);
  159. });
  160. // 验证包含大巴包车和商务车包车路线
  161. const routeNames = responseData.data.routes.map((route: any) => route.name);
  162. expect(routeNames).toContain('大巴包车路线');
  163. expect(routeNames).toContain('商务车包车路线');
  164. // 验证不包含拼车路线
  165. expect(routeNames).not.toContain('大巴拼车路线1');
  166. expect(routeNames).not.toContain('商务车拼车路线');
  167. }
  168. });
  169. it('应该支持多值车型参数查询', async () => {
  170. const response = await client.routes.search.$get({
  171. query: {
  172. vehicleType: 'bus,minibus',
  173. travelMode: 'carpool'
  174. }
  175. });
  176. IntegrationTestAssertions.expectStatus(response, 200);
  177. if (response.status === 200) {
  178. const responseData = await response.json();
  179. expect(responseData.success).toBe(true);
  180. // 验证包含大巴拼车和中巴拼车路线
  181. const routeNames = responseData.data.routes.map((route: any) => route.name);
  182. expect(routeNames).toContain('大巴拼车路线1');
  183. expect(routeNames).toContain('大巴拼车路线2');
  184. expect(routeNames).toContain('中巴拼车路线');
  185. // 验证不包含商务车和小车路线
  186. expect(routeNames).not.toContain('商务车拼车路线');
  187. expect(routeNames).not.toContain('小车包车路线');
  188. }
  189. });
  190. it('应该支持多值出行方式参数查询', async () => {
  191. const response = await client.routes.search.$get({
  192. query: {
  193. vehicleType: 'business',
  194. travelMode: 'carpool,charter'
  195. }
  196. });
  197. IntegrationTestAssertions.expectStatus(response, 200);
  198. if (response.status === 200) {
  199. const responseData = await response.json();
  200. expect(responseData.success).toBe(true);
  201. // 验证包含商务车拼车和包车路线
  202. const routeNames = responseData.data.routes.map((route: any) => route.name);
  203. expect(routeNames).toContain('商务车拼车路线');
  204. expect(routeNames).toContain('商务车包车路线');
  205. }
  206. });
  207. it('应该正确处理单个参数查询', async () => {
  208. // 只查询车型
  209. const response1 = await client.routes.search.$get({
  210. query: {
  211. vehicleType: 'bus'
  212. }
  213. });
  214. IntegrationTestAssertions.expectStatus(response1, 200);
  215. if (response1.status === 200) {
  216. const responseData = await response1.json();
  217. expect(responseData.success).toBe(true);
  218. // 验证包含所有大巴路线(拼车和包车)
  219. const routeNames = responseData.data.routes.map((route: any) => route.name);
  220. expect(routeNames).toContain('大巴拼车路线1');
  221. expect(routeNames).toContain('大巴拼车路线2');
  222. expect(routeNames).toContain('大巴包车路线');
  223. }
  224. // 只查询出行方式
  225. const response2 = await client.routes.search.$get({
  226. query: {
  227. travelMode: 'carpool'
  228. }
  229. });
  230. IntegrationTestAssertions.expectStatus(response2, 200);
  231. if (response2.status === 200) {
  232. const responseData = await response2.json();
  233. expect(responseData.success).toBe(true);
  234. // 验证包含所有拼车路线
  235. const routeNames = responseData.data.routes.map((route: any) => route.name);
  236. expect(routeNames).toContain('大巴拼车路线1');
  237. expect(routeNames).toContain('大巴拼车路线2');
  238. expect(routeNames).toContain('商务车拼车路线');
  239. expect(routeNames).toContain('中巴拼车路线');
  240. }
  241. });
  242. it('应该正确处理空参数查询', async () => {
  243. const response = await client.routes.search.$get({
  244. query: {}
  245. });
  246. IntegrationTestAssertions.expectStatus(response, 200);
  247. if (response.status === 200) {
  248. const responseData = await response.json();
  249. expect(responseData.success).toBe(true);
  250. // 验证返回所有路线
  251. expect(responseData.data.routes.length).toBeGreaterThan(0);
  252. }
  253. });
  254. it('应该正确处理无效参数', async () => {
  255. // 无效车型参数
  256. const response1 = await client.routes.search.$get({
  257. query: {
  258. vehicleType: 'invalid_vehicle'
  259. }
  260. });
  261. IntegrationTestAssertions.expectStatus(response1, 200);
  262. if (response1.status === 200) {
  263. const responseData = await response1.json();
  264. expect(responseData.success).toBe(true);
  265. // 无效参数应该被忽略,返回所有路线
  266. expect(responseData.data.routes.length).toBeGreaterThan(0);
  267. }
  268. // 无效出行方式参数
  269. const response2 = await client.routes.search.$get({
  270. query: {
  271. travelMode: 'invalid_mode'
  272. }
  273. });
  274. IntegrationTestAssertions.expectStatus(response2, 200);
  275. if (response2.status === 200) {
  276. const responseData = await response2.json();
  277. expect(responseData.success).toBe(true);
  278. // 无效参数应该被忽略,返回所有路线
  279. expect(responseData.data.routes.length).toBeGreaterThan(0);
  280. }
  281. });
  282. });
  283. describe('组合查询与其他筛选条件结合测试', () => {
  284. it('应该支持组合查询与价格筛选结合', async () => {
  285. const response = await client.routes.search.$get({
  286. query: {
  287. vehicleType: 'bus',
  288. travelMode: 'carpool',
  289. minPrice: '110',
  290. maxPrice: '130'
  291. }
  292. });
  293. IntegrationTestAssertions.expectStatus(response, 200);
  294. if (response.status === 200) {
  295. const responseData = await response.json();
  296. expect(responseData.success).toBe(true);
  297. // 验证只返回价格在110-130之间的大巴拼车路线
  298. const routeNames = responseData.data.routes.map((route: any) => route.name);
  299. expect(routeNames).toContain('大巴拼车路线2'); // 价格120
  300. expect(routeNames).not.toContain('大巴拼车路线1'); // 价格100
  301. // 验证价格范围
  302. responseData.data.routes.forEach((route: any) => {
  303. expect(route.price).toBeGreaterThanOrEqual(110);
  304. expect(route.price).toBeLessThanOrEqual(130);
  305. });
  306. }
  307. });
  308. it('应该支持组合查询与活动筛选结合', async () => {
  309. const dataSource = await IntegrationTestDatabase.getDataSource();
  310. if (!dataSource) throw new Error('Database not initialized');
  311. const activity = await TestDataFactory.createTestActivity(dataSource, {
  312. name: '特定活动'
  313. });
  314. // 为特定活动创建路线
  315. await TestDataFactory.createTestRoute(dataSource, {
  316. name: '特定活动大巴拼车路线',
  317. vehicleType: VehicleType.BUS,
  318. travelMode: TravelMode.CARPOOL,
  319. activityId: activity.id,
  320. price: 150
  321. });
  322. const response = await client.routes.search.$get({
  323. query: {
  324. vehicleType: 'bus',
  325. travelMode: 'carpool'
  326. }
  327. });
  328. IntegrationTestAssertions.expectStatus(response, 200);
  329. if (response.status === 200) {
  330. const responseData = await response.json();
  331. expect(responseData.success).toBe(true);
  332. // 验证包含特定活动的路线
  333. const routeNames = responseData.data.routes.map((route: any) => route.name);
  334. expect(routeNames).toContain('特定活动大巴拼车路线');
  335. }
  336. });
  337. });
  338. describe('分页和排序测试', () => {
  339. it('应该支持组合查询的分页', async () => {
  340. const response = await client.routes.search.$get({
  341. query: {
  342. vehicleType: 'bus',
  343. travelMode: 'carpool',
  344. page: '1',
  345. pageSize: '1'
  346. }
  347. });
  348. IntegrationTestAssertions.expectStatus(response, 200);
  349. if (response.status === 200) {
  350. const responseData = await response.json();
  351. expect(responseData.success).toBe(true);
  352. // 验证分页信息
  353. expect(responseData.data.pagination.page).toBe(1);
  354. expect(responseData.data.pagination.pageSize).toBe(1);
  355. expect(responseData.data.pagination.total).toBe(2);
  356. expect(responseData.data.pagination.totalPages).toBe(2);
  357. // 验证只返回1条路线
  358. expect(responseData.data.routes).toHaveLength(1);
  359. }
  360. });
  361. it('应该支持组合查询的价格排序', async () => {
  362. const response = await client.routes.search.$get({
  363. query: {
  364. vehicleType: 'bus',
  365. travelMode: 'carpool',
  366. sortBy: 'price',
  367. sortOrder: 'ASC'
  368. }
  369. });
  370. IntegrationTestAssertions.expectStatus(response, 200);
  371. if (response.status === 200) {
  372. const responseData = await response.json();
  373. expect(responseData.success).toBe(true);
  374. // 验证路线按价格升序排列
  375. const prices = responseData.data.routes.map((route: any) => route.price);
  376. expect(prices[0]).toBeLessThanOrEqual(prices[1]);
  377. }
  378. });
  379. });
  380. });