|
|
@@ -0,0 +1,444 @@
|
|
|
+import { describe, it, expect, beforeEach } from 'vitest';
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
+import {
|
|
|
+ IntegrationTestDatabase,
|
|
|
+ setupIntegrationDatabaseHooks,
|
|
|
+ TestDataFactory
|
|
|
+} from '~/utils/server/integration-test-db';
|
|
|
+import { IntegrationTestAssertions } from '~/utils/server/integration-test-utils';
|
|
|
+import { routesRoutesExport } from '@/server/api';
|
|
|
+import { VehicleType, TravelMode } from '@/server/modules/routes/route.schema';
|
|
|
+
|
|
|
+// 设置集成测试钩子
|
|
|
+setupIntegrationDatabaseHooks()
|
|
|
+
|
|
|
+describe('用户端路线搜索API集成测试', () => {
|
|
|
+ let client: ReturnType<typeof testClient<typeof routesRoutesExport>>['api']['v1'];
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 创建测试客户端
|
|
|
+ client = testClient(routesRoutesExport).api.v1;
|
|
|
+
|
|
|
+ // 创建测试数据
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ // 创建测试活动
|
|
|
+ const activity1 = await TestDataFactory.createTestActivity(dataSource, {
|
|
|
+ name: '测试活动1'
|
|
|
+ });
|
|
|
+ const activity2 = await TestDataFactory.createTestActivity(dataSource, {
|
|
|
+ name: '测试活动2'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建测试路线,覆盖所有组合查询场景
|
|
|
+ // 大巴拼车路线
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '大巴拼车路线1',
|
|
|
+ vehicleType: VehicleType.BUS,
|
|
|
+ travelMode: TravelMode.CARPOOL,
|
|
|
+ activityId: activity1.id,
|
|
|
+ price: 100,
|
|
|
+ availableSeats: 30
|
|
|
+ });
|
|
|
+
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '大巴拼车路线2',
|
|
|
+ vehicleType: VehicleType.BUS,
|
|
|
+ travelMode: TravelMode.CARPOOL,
|
|
|
+ activityId: activity2.id,
|
|
|
+ price: 120,
|
|
|
+ availableSeats: 25
|
|
|
+ });
|
|
|
+
|
|
|
+ // 商务车路线(拼车)
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '商务车拼车路线',
|
|
|
+ vehicleType: VehicleType.BUSINESS,
|
|
|
+ travelMode: TravelMode.CARPOOL,
|
|
|
+ activityId: activity1.id,
|
|
|
+ price: 200,
|
|
|
+ availableSeats: 4
|
|
|
+ });
|
|
|
+
|
|
|
+ // 商务车路线(包车)
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '商务车包车路线',
|
|
|
+ vehicleType: VehicleType.BUSINESS,
|
|
|
+ travelMode: TravelMode.CHARTER,
|
|
|
+ activityId: activity2.id,
|
|
|
+ price: 500,
|
|
|
+ availableSeats: 4
|
|
|
+ });
|
|
|
+
|
|
|
+ // 大巴包车路线
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '大巴包车路线',
|
|
|
+ vehicleType: VehicleType.BUS,
|
|
|
+ travelMode: TravelMode.CHARTER,
|
|
|
+ activityId: activity1.id,
|
|
|
+ price: 800,
|
|
|
+ availableSeats: 40
|
|
|
+ });
|
|
|
+
|
|
|
+ // 中巴拼车路线(用于验证排除逻辑)
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '中巴拼车路线',
|
|
|
+ vehicleType: VehicleType.MINIBUS,
|
|
|
+ travelMode: TravelMode.CARPOOL,
|
|
|
+ activityId: activity1.id,
|
|
|
+ price: 150,
|
|
|
+ availableSeats: 15
|
|
|
+ });
|
|
|
+
|
|
|
+ // 小车包车路线(用于验证排除逻辑)
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '小车包车路线',
|
|
|
+ vehicleType: VehicleType.CAR,
|
|
|
+ travelMode: TravelMode.CHARTER,
|
|
|
+ activityId: activity2.id,
|
|
|
+ price: 300,
|
|
|
+ availableSeats: 3
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('组合查询逻辑测试', () => {
|
|
|
+ it('应该正确查询大巴拼车组合', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus',
|
|
|
+ travelMode: 'carpool'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+ expect(responseData.data.routes).toHaveLength(2);
|
|
|
+
|
|
|
+ // 验证所有返回路线都是大巴拼车
|
|
|
+ responseData.data.routes.forEach((route: any) => {
|
|
|
+ expect(route.vehicleType).toBe(VehicleType.BUS);
|
|
|
+ expect(route.travelMode).toBe(TravelMode.CARPOOL);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证包含正确的路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).toContain('大巴拼车路线2');
|
|
|
+
|
|
|
+ // 验证不包含其他类型的路线
|
|
|
+ expect(routeNames).not.toContain('商务车拼车路线');
|
|
|
+ expect(routeNames).not.toContain('大巴包车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该正确查询商务车组合(支持拼车和包车)', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'business',
|
|
|
+ travelMode: 'carpool,charter'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+ expect(responseData.data.routes).toHaveLength(2);
|
|
|
+
|
|
|
+ // 验证所有返回路线都是商务车
|
|
|
+ responseData.data.routes.forEach((route: any) => {
|
|
|
+ expect(route.vehicleType).toBe(VehicleType.BUSINESS);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证包含商务车拼车和包车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('商务车拼车路线');
|
|
|
+ expect(routeNames).toContain('商务车包车路线');
|
|
|
+
|
|
|
+ // 验证不包含其他车型的路线
|
|
|
+ expect(routeNames).not.toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).not.toContain('中巴拼车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该正确查询包车组合(支持大巴和商务车)', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus,business',
|
|
|
+ travelMode: 'charter'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+ expect(responseData.data.routes).toHaveLength(2);
|
|
|
+
|
|
|
+ // 验证所有返回路线都是包车
|
|
|
+ responseData.data.routes.forEach((route: any) => {
|
|
|
+ expect(route.travelMode).toBe(TravelMode.CHARTER);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证包含大巴包车和商务车包车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴包车路线');
|
|
|
+ expect(routeNames).toContain('商务车包车路线');
|
|
|
+
|
|
|
+ // 验证不包含拼车路线
|
|
|
+ expect(routeNames).not.toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).not.toContain('商务车拼车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该支持多值车型参数查询', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus,minibus',
|
|
|
+ travelMode: 'carpool'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证包含大巴拼车和中巴拼车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).toContain('大巴拼车路线2');
|
|
|
+ expect(routeNames).toContain('中巴拼车路线');
|
|
|
+
|
|
|
+ // 验证不包含商务车和小车路线
|
|
|
+ expect(routeNames).not.toContain('商务车拼车路线');
|
|
|
+ expect(routeNames).not.toContain('小车包车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该支持多值出行方式参数查询', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'business',
|
|
|
+ travelMode: 'carpool,charter'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证包含商务车拼车和包车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('商务车拼车路线');
|
|
|
+ expect(routeNames).toContain('商务车包车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该正确处理单个参数查询', async () => {
|
|
|
+ // 只查询车型
|
|
|
+ const response1 = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response1, 200);
|
|
|
+ if (response1.status === 200) {
|
|
|
+ const responseData = await response1.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证包含所有大巴路线(拼车和包车)
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).toContain('大巴拼车路线2');
|
|
|
+ expect(routeNames).toContain('大巴包车路线');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只查询出行方式
|
|
|
+ const response2 = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ travelMode: 'carpool'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response2, 200);
|
|
|
+ if (response2.status === 200) {
|
|
|
+ const responseData = await response2.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证包含所有拼车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴拼车路线1');
|
|
|
+ expect(routeNames).toContain('大巴拼车路线2');
|
|
|
+ expect(routeNames).toContain('商务车拼车路线');
|
|
|
+ expect(routeNames).toContain('中巴拼车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该正确处理空参数查询', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {}
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证返回所有路线
|
|
|
+ expect(responseData.data.routes.length).toBeGreaterThan(0);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该正确处理无效参数', async () => {
|
|
|
+ // 无效车型参数
|
|
|
+ const response1 = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'invalid_vehicle'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response1, 200);
|
|
|
+ if (response1.status === 200) {
|
|
|
+ const responseData = await response1.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+ // 无效参数应该被忽略,返回所有路线
|
|
|
+ expect(responseData.data.routes.length).toBeGreaterThan(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 无效出行方式参数
|
|
|
+ const response2 = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ travelMode: 'invalid_mode'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response2, 200);
|
|
|
+ if (response2.status === 200) {
|
|
|
+ const responseData = await response2.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+ // 无效参数应该被忽略,返回所有路线
|
|
|
+ expect(responseData.data.routes.length).toBeGreaterThan(0);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('组合查询与其他筛选条件结合测试', () => {
|
|
|
+ it('应该支持组合查询与价格筛选结合', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus',
|
|
|
+ travelMode: 'carpool',
|
|
|
+ minPrice: '110',
|
|
|
+ maxPrice: '130'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证只返回价格在110-130之间的大巴拼车路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('大巴拼车路线2'); // 价格120
|
|
|
+ expect(routeNames).not.toContain('大巴拼车路线1'); // 价格100
|
|
|
+
|
|
|
+ // 验证价格范围
|
|
|
+ responseData.data.routes.forEach((route: any) => {
|
|
|
+ expect(route.price).toBeGreaterThanOrEqual(110);
|
|
|
+ expect(route.price).toBeLessThanOrEqual(130);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该支持组合查询与活动筛选结合', async () => {
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ if (!dataSource) throw new Error('Database not initialized');
|
|
|
+
|
|
|
+ const activity = await TestDataFactory.createTestActivity(dataSource, {
|
|
|
+ name: '特定活动'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 为特定活动创建路线
|
|
|
+ await TestDataFactory.createTestRoute(dataSource, {
|
|
|
+ name: '特定活动大巴拼车路线',
|
|
|
+ vehicleType: VehicleType.BUS,
|
|
|
+ travelMode: TravelMode.CARPOOL,
|
|
|
+ activityId: activity.id,
|
|
|
+ price: 150
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus',
|
|
|
+ travelMode: 'carpool'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证包含特定活动的路线
|
|
|
+ const routeNames = responseData.data.routes.map((route: any) => route.name);
|
|
|
+ expect(routeNames).toContain('特定活动大巴拼车路线');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('分页和排序测试', () => {
|
|
|
+ it('应该支持组合查询的分页', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus',
|
|
|
+ travelMode: 'carpool',
|
|
|
+ page: '1',
|
|
|
+ pageSize: '1'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证分页信息
|
|
|
+ expect(responseData.data.pagination.page).toBe(1);
|
|
|
+ expect(responseData.data.pagination.pageSize).toBe(1);
|
|
|
+ expect(responseData.data.pagination.total).toBe(2);
|
|
|
+ expect(responseData.data.pagination.totalPages).toBe(2);
|
|
|
+
|
|
|
+ // 验证只返回1条路线
|
|
|
+ expect(responseData.data.routes).toHaveLength(1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该支持组合查询的价格排序', async () => {
|
|
|
+ const response = await client.routes.search.$get({
|
|
|
+ query: {
|
|
|
+ vehicleType: 'bus',
|
|
|
+ travelMode: 'carpool',
|
|
|
+ sortBy: 'price',
|
|
|
+ sortOrder: 'ASC'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ IntegrationTestAssertions.expectStatus(response, 200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const responseData = await response.json();
|
|
|
+ expect(responseData.success).toBe(true);
|
|
|
+
|
|
|
+ // 验证路线按价格升序排列
|
|
|
+ const prices = responseData.data.routes.map((route: any) => route.price);
|
|
|
+ expect(prices[0]).toBeLessThanOrEqual(prices[1]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|