routes.integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  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 { adminRoutesRoutesExport } from '@/server/api';
  10. import { AuthService } from '@/server/modules/auth/auth.service';
  11. import { UserService } from '@/server/modules/users/user.service';
  12. import { VehicleType } from '@/server/modules/routes/route.schema';
  13. // 设置集成测试钩子
  14. setupIntegrationDatabaseHooks()
  15. describe('路线管理API集成测试', () => {
  16. let client: ReturnType<typeof testClient<typeof adminRoutesRoutesExport>>['api']['v1']['admin'];
  17. let testToken: string;
  18. beforeEach(async () => {
  19. // 创建测试客户端
  20. client = testClient(adminRoutesRoutesExport).api.v1.admin;
  21. // 创建测试用户并生成token
  22. const dataSource = await IntegrationTestDatabase.getDataSource();
  23. const userService = new UserService(dataSource);
  24. const authService = new AuthService(userService);
  25. // 确保admin用户存在
  26. const user = await authService.ensureAdminExists();
  27. // 生成admin用户的token
  28. testToken = authService.generateToken(user);
  29. });
  30. describe('路线创建测试', () => {
  31. it('应该成功创建路线', async () => {
  32. const dataSource = await IntegrationTestDatabase.getDataSource();
  33. if (!dataSource) throw new Error('Database not initialized');
  34. // 先创建一个活动
  35. const testActivity = await TestDataFactory.createTestActivity(dataSource);
  36. const routeData = {
  37. name: '测试路线',
  38. startLocationId: 1,
  39. endLocationId: 2,
  40. pickupPoint: '北京西站',
  41. dropoffPoint: '上海南站',
  42. departureTime: '2025-10-17T08:00:00.000Z',
  43. vehicleType: VehicleType.BUS,
  44. price: 200,
  45. seatCount: 40,
  46. availableSeats: 40,
  47. activityId: testActivity.id
  48. };
  49. const response = await client.routes.$post({
  50. json: routeData,
  51. },
  52. {
  53. headers: {
  54. 'Authorization': `Bearer ${testToken}`
  55. }
  56. });
  57. // 断言响应
  58. expect(response.status).toBe(201);
  59. if (response.status === 201) {
  60. const responseData = await response.json();
  61. expect(responseData).toHaveProperty('id');
  62. expect(responseData.name).toBe(routeData.name);
  63. expect(responseData.pickupPoint).toBe(routeData.pickupPoint);
  64. expect(responseData.dropoffPoint).toBe(routeData.dropoffPoint);
  65. expect(responseData.vehicleType).toBe(routeData.vehicleType);
  66. expect(responseData.price).toBe(routeData.price);
  67. expect(responseData.seatCount).toBe(routeData.seatCount);
  68. expect(responseData.availableSeats).toBe(routeData.availableSeats);
  69. expect(responseData.isDisabled).toBe(0); // 默认启用
  70. // 断言数据库中存在路线
  71. await IntegrationTestAssertions.expectRouteToExist(responseData.id);
  72. }
  73. });
  74. it('应该拒绝创建无效车型的路线', async () => {
  75. const dataSource = await IntegrationTestDatabase.getDataSource();
  76. if (!dataSource) throw new Error('Database not initialized');
  77. const testActivity = await TestDataFactory.createTestActivity(dataSource);
  78. const routeData = {
  79. name: '测试路线',
  80. startLocationId: 1,
  81. endLocationId: 2,
  82. pickupPoint: '北京西站',
  83. dropoffPoint: '上海南站',
  84. departureTime: '2025-10-17T08:00:00.000Z',
  85. vehicleType: 'invalid_vehicle' as any, // 无效车型,应该验证失败
  86. price: 200,
  87. seatCount: 40,
  88. availableSeats: 40,
  89. activityId: testActivity.id
  90. };
  91. const response = await client.routes.$post({
  92. json: routeData,
  93. },
  94. {
  95. headers: {
  96. 'Authorization': `Bearer ${testToken}`
  97. }
  98. });
  99. // 应该返回验证错误
  100. expect([400, 500]).toContain(response.status);
  101. });
  102. it('应该拒绝创建价格为负数的路线', async () => {
  103. const dataSource = await IntegrationTestDatabase.getDataSource();
  104. if (!dataSource) throw new Error('Database not initialized');
  105. const testActivity = await TestDataFactory.createTestActivity(dataSource);
  106. const routeData = {
  107. name: '测试路线',
  108. startLocationId: 1,
  109. endLocationId: 2,
  110. pickupPoint: '北京西站',
  111. dropoffPoint: '上海南站',
  112. departureTime: '2025-10-17T08:00:00.000Z',
  113. vehicleType: VehicleType.BUS,
  114. price: -100, // 负数价格
  115. seatCount: 40,
  116. availableSeats: 40,
  117. activityId: testActivity.id
  118. };
  119. const response = await client.routes.$post({
  120. json: routeData,
  121. },
  122. {
  123. headers: {
  124. 'Authorization': `Bearer ${testToken}`
  125. }
  126. });
  127. // 应该返回验证错误
  128. expect([400, 500]).toContain(response.status);
  129. });
  130. });
  131. describe('路线读取测试', () => {
  132. it('应该成功获取路线列表', async () => {
  133. const dataSource = await IntegrationTestDatabase.getDataSource();
  134. if (!dataSource) throw new Error('Database not initialized');
  135. // 创建几个测试路线
  136. await TestDataFactory.createTestRoute(dataSource, { name: '路线1', vehicleType: VehicleType.BUS });
  137. await TestDataFactory.createTestRoute(dataSource, { name: '路线2', vehicleType: VehicleType.MINIBUS });
  138. const response = await client.routes.$get({
  139. query: {}
  140. },
  141. {
  142. headers: {
  143. 'Authorization': `Bearer ${testToken}`
  144. }
  145. });
  146. expect(response.status).toBe(200);
  147. if (response.status === 200) {
  148. const responseData = await response.json();
  149. expect(Array.isArray(responseData.data)).toBe(true);
  150. expect(responseData.data.length).toBeGreaterThanOrEqual(2);
  151. }
  152. });
  153. it('应该成功获取单个路线详情', async () => {
  154. const dataSource = await IntegrationTestDatabase.getDataSource();
  155. if (!dataSource) throw new Error('Database not initialized');
  156. const testRoute = await TestDataFactory.createTestRoute(dataSource, {
  157. name: '测试路线详情'
  158. });
  159. const response = await client.routes[':id'].$get({
  160. param: { id: testRoute.id }
  161. },
  162. {
  163. headers: {
  164. 'Authorization': `Bearer ${testToken}`
  165. }
  166. });
  167. expect(response.status).toBe(200);
  168. if (response.status === 200) {
  169. const responseData = await response.json();
  170. expect(responseData.id).toBe(testRoute.id);
  171. expect(responseData.name).toBe(testRoute.name);
  172. expect(responseData.vehicleType).toBe(testRoute.vehicleType);
  173. }
  174. });
  175. it('应该返回404当路线不存在时', async () => {
  176. const response = await client.routes[':id'].$get({
  177. param: { id: 999999 }
  178. },
  179. {
  180. headers: {
  181. 'Authorization': `Bearer ${testToken}`
  182. }
  183. });
  184. expect(response.status).toBe(404);
  185. if (response.status === 404) {
  186. const responseData = await response.json();
  187. expect(responseData.message).toContain('资源不存在');
  188. }
  189. });
  190. });
  191. describe('路线更新测试', () => {
  192. it('应该成功更新路线信息', async () => {
  193. const dataSource = await IntegrationTestDatabase.getDataSource();
  194. if (!dataSource) throw new Error('Database not initialized');
  195. const testRoute = await TestDataFactory.createTestRoute(dataSource, {
  196. name: '测试路线更新'
  197. });
  198. const updateData = {
  199. name: '更新后的路线名称',
  200. startLocationId: 3,
  201. price: 300
  202. };
  203. const response = await client.routes[':id'].$put({
  204. param: { id: testRoute.id },
  205. json: updateData
  206. },
  207. {
  208. headers: {
  209. 'Authorization': `Bearer ${testToken}`
  210. }
  211. });
  212. expect(response.status).toBe(200);
  213. if (response.status === 200) {
  214. const responseData = await response.json();
  215. expect(responseData.name).toBe(updateData.name);
  216. expect(responseData.startLocationId).toBe(updateData.startLocationId);
  217. expect(parseFloat(responseData.price as unknown as string)).toBe(updateData.price);
  218. }
  219. // 验证数据库中的更新
  220. const getResponse = await client.routes[':id'].$get({
  221. param: { id: testRoute.id }
  222. },
  223. {
  224. headers: {
  225. 'Authorization': `Bearer ${testToken}`
  226. }
  227. });
  228. if (getResponse.status === 200) {
  229. expect(getResponse.status).toBe(200);
  230. const getResponseData = await getResponse.json();
  231. expect(getResponseData.name).toBe(updateData.name);
  232. }
  233. });
  234. it('应该成功启用/禁用路线', async () => {
  235. const dataSource = await IntegrationTestDatabase.getDataSource();
  236. if (!dataSource) throw new Error('Database not initialized');
  237. const testRoute = await TestDataFactory.createTestRoute(dataSource, {
  238. name: '测试状态切换',
  239. isDisabled: 0 // 启用状态
  240. });
  241. // 禁用路线
  242. const disableResponse = await client.routes[':id'].$put({
  243. param: { id: testRoute.id },
  244. json: { isDisabled: 1 } // 禁用
  245. },
  246. {
  247. headers: {
  248. 'Authorization': `Bearer ${testToken}`
  249. }
  250. });
  251. expect(disableResponse.status).toBe(200);
  252. if (disableResponse.status === 200) {
  253. const disableData = await disableResponse.json();
  254. expect(disableData.isDisabled).toBe(1);
  255. }
  256. // 重新启用路线
  257. const enableResponse = await client.routes[':id'].$put({
  258. param: { id: testRoute.id },
  259. json: { isDisabled: 0 } // 启用
  260. },
  261. {
  262. headers: {
  263. 'Authorization': `Bearer ${testToken}`
  264. }
  265. });
  266. expect(enableResponse.status).toBe(200);
  267. if (enableResponse.status === 200) {
  268. const enableData = await enableResponse.json();
  269. expect(enableData.isDisabled).toBe(0);
  270. }
  271. });
  272. it('应该返回404当更新不存在的路线时', async () => {
  273. const updateData = {
  274. name: '更新后的名称'
  275. };
  276. const response = await client.routes[':id'].$put({
  277. param: { id: 999999 },
  278. json: updateData
  279. },
  280. {
  281. headers: {
  282. 'Authorization': `Bearer ${testToken}`
  283. }
  284. });
  285. expect(response.status).toBe(404);
  286. if (response.status === 404) {
  287. const responseData = await response.json();
  288. expect(responseData.message).toContain('资源不存在');
  289. }
  290. });
  291. });
  292. describe('路线删除测试', () => {
  293. it('应该成功删除路线', async () => {
  294. const dataSource = await IntegrationTestDatabase.getDataSource();
  295. if (!dataSource) throw new Error('Database not initialized');
  296. const testRoute = await TestDataFactory.createTestRoute(dataSource, {
  297. name: '测试路线删除'
  298. });
  299. const response = await client.routes[':id'].$delete({
  300. param: { id: testRoute.id }
  301. },
  302. {
  303. headers: {
  304. 'Authorization': `Bearer ${testToken}`
  305. }
  306. });
  307. IntegrationTestAssertions.expectStatus(response, 204);
  308. // 验证路线已从数据库中删除
  309. await IntegrationTestAssertions.expectRouteNotToExist(testRoute.id);
  310. // 验证再次获取路线返回404
  311. const getResponse = await client.routes[':id'].$get({
  312. param: { id: testRoute.id }
  313. },
  314. {
  315. headers: {
  316. 'Authorization': `Bearer ${testToken}`
  317. }
  318. });
  319. IntegrationTestAssertions.expectStatus(getResponse, 404);
  320. });
  321. it('应该返回404当删除不存在的路线时', async () => {
  322. const response = await client.routes[':id'].$delete({
  323. param: { id: 999999 }
  324. },
  325. {
  326. headers: {
  327. 'Authorization': `Bearer ${testToken}`
  328. }
  329. });
  330. IntegrationTestAssertions.expectStatus(response, 404);
  331. if (response.status === 404) {
  332. const responseData = await response.json();
  333. expect(responseData.message).toContain('资源不存在');
  334. }
  335. });
  336. });
  337. describe('路线搜索测试', () => {
  338. it('应该能够按路线名称搜索路线', async () => {
  339. const dataSource = await IntegrationTestDatabase.getDataSource();
  340. if (!dataSource) throw new Error('Database not initialized');
  341. await TestDataFactory.createTestRoute(dataSource, { name: '搜索路线1', startLocationId: 1 });
  342. await TestDataFactory.createTestRoute(dataSource, { name: '搜索路线2', startLocationId: 2 });
  343. await TestDataFactory.createTestRoute(dataSource, { name: '其他路线', startLocationId: 3 });
  344. const response = await client.routes.$get({
  345. query: { keyword: '搜索路线' }
  346. },
  347. {
  348. headers: {
  349. 'Authorization': `Bearer ${testToken}`
  350. }
  351. });
  352. IntegrationTestAssertions.expectStatus(response, 200);
  353. if (response.status === 200) {
  354. const responseData = await response.json();
  355. expect(Array.isArray(responseData.data)).toBe(true);
  356. expect(responseData.data.length).toBe(2);
  357. // 验证搜索结果包含正确的路线
  358. const names = responseData.data.map((route) => route.name);
  359. expect(names).toContain('搜索路线1');
  360. expect(names).toContain('搜索路线2');
  361. expect(names).not.toContain('其他路线');
  362. }
  363. });
  364. it('应该能够按出发地搜索路线', async () => {
  365. const dataSource = await IntegrationTestDatabase.getDataSource();
  366. if (!dataSource) throw new Error('Database not initialized');
  367. await TestDataFactory.createTestRoute(dataSource, { name: '路线1', startLocationId: 1 });
  368. await TestDataFactory.createTestRoute(dataSource, { name: '路线2', startLocationId: 1 });
  369. const response = await client.routes.$get({
  370. query: { keyword: '北京' }
  371. },
  372. {
  373. headers: {
  374. 'Authorization': `Bearer ${testToken}`
  375. }
  376. });
  377. IntegrationTestAssertions.expectStatus(response, 200);
  378. if (response.status === 200) {
  379. const responseData = await response.json();
  380. expect(responseData.data.length).toBe(2);
  381. const names = responseData.data.map((route) => route.name);
  382. expect(names).toContain('路线1');
  383. expect(names).toContain('路线2');
  384. }
  385. });
  386. it('应该能够按车型筛选路线', async () => {
  387. const dataSource = await IntegrationTestDatabase.getDataSource();
  388. if (!dataSource) throw new Error('Database not initialized');
  389. await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线1', vehicleType: VehicleType.BUS });
  390. await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线2', vehicleType: VehicleType.BUS });
  391. await TestDataFactory.createTestRoute(dataSource, { name: '中巴路线', vehicleType: VehicleType.MINIBUS });
  392. const response = await client.routes.$get({
  393. query: { filters: JSON.stringify({ vehicleType: 'bus' }) }
  394. },
  395. {
  396. headers: {
  397. 'Authorization': `Bearer ${testToken}`
  398. }
  399. });
  400. IntegrationTestAssertions.expectStatus(response, 200);
  401. if (response.status === 200) {
  402. const responseData = await response.json();
  403. expect(responseData.data.length).toBe(2);
  404. const vehicleTypes = responseData.data.map((route) => route.vehicleType);
  405. expect(vehicleTypes.every((type: string) => type === 'bus')).toBe(true);
  406. }
  407. });
  408. });
  409. describe('性能测试', () => {
  410. it('路线列表查询响应时间应小于200ms', async () => {
  411. const dataSource = await IntegrationTestDatabase.getDataSource();
  412. if (!dataSource) throw new Error('Database not initialized');
  413. // 创建一些测试数据
  414. for (let i = 0; i < 10; i++) {
  415. await TestDataFactory.createTestRoute(dataSource, {
  416. name: `性能测试路线_${i}`,
  417. startLocationId: i + 1,
  418. endLocationId: i + 2
  419. });
  420. }
  421. const startTime = Date.now();
  422. const response = await client.routes.$get({
  423. query: {}
  424. },
  425. {
  426. headers: {
  427. 'Authorization': `Bearer ${testToken}`
  428. }
  429. });
  430. const endTime = Date.now();
  431. const responseTime = endTime - startTime;
  432. IntegrationTestAssertions.expectStatus(response, 200);
  433. expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
  434. });
  435. });
  436. });