routes.integration.test.ts 16 KB

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