providers-actions.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. /**
  2. * 供应商管理模块 API 测试
  3. *
  4. * ⚠️ 状态:待重构为集成测试
  5. * 详见:tests/DIAGNOSIS-FINAL.md
  6. *
  7. * ---
  8. *
  9. * 测试范围:
  10. * - getProviders() - 获取供应商列表
  11. * - addProvider() - 添加供应商
  12. * - editProvider() - 编辑供应商
  13. * - removeProvider() - 删除供应商
  14. * - getProvidersHealthStatus() - 获取熔断器健康状态
  15. * - resetProviderCircuit() - 重置熔断器
  16. * - getProviderLimitUsage() - 获取供应商限额使用情况
  17. * - testProviderProxy() - 测试代理连接
  18. * - getUnmaskedProviderKey() - 获取完整密钥
  19. *
  20. * 测试场景:
  21. * - CRUD 操作
  22. * - 权重和优先级验证
  23. * - 代理配置验证
  24. * - 熔断器状态管理
  25. */
  26. import { beforeEach, describe, expect, test } from "vitest";
  27. import { callActionsRoute } from "../test-utils";
  28. const ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || "test-admin-token";
  29. const USER_TOKEN = "test-user-token";
  30. // 辅助函数:调用供应商管理 API
  31. async function callProvidersApi(
  32. action: string,
  33. body: Record<string, unknown> = {},
  34. authToken = ADMIN_TOKEN
  35. ) {
  36. const { response, json } = await callActionsRoute({
  37. method: "POST",
  38. pathname: `/api/actions/providers/${action}`,
  39. authToken,
  40. body,
  41. });
  42. return { response, data: json as any };
  43. }
  44. // ⚠️ 跳过所有测试直到重构为集成测试
  45. describe.skip("供应商管理 - API 测试(待重构)", () => {
  46. test("未登录应返回空数组", async () => {
  47. const { response, data } = await callProvidersApi("getProviders", {}, undefined);
  48. expect(response.ok).toBe(true);
  49. expect(Array.isArray(data)).toBe(true);
  50. expect(data.length).toBe(0);
  51. });
  52. test("管理员应该可以查看所有供应商", async () => {
  53. const { response, data } = await callProvidersApi("getProviders");
  54. expect(response.ok).toBe(true);
  55. expect(Array.isArray(data)).toBe(true);
  56. });
  57. test("普通用户不能查看供应商列表", async () => {
  58. const { response, data } = await callProvidersApi("getProviders", {}, USER_TOKEN);
  59. expect(response.ok).toBe(true);
  60. expect(Array.isArray(data)).toBe(true);
  61. expect(data.length).toBe(0);
  62. });
  63. });
  64. describe.skip("供应商管理 - 添加供应商 (addProvider)", () => {
  65. test("应该成功添加 Claude 供应商", async () => {
  66. const { response, data } = await callProvidersApi("addProvider", {
  67. name: `测试供应商_Claude_${Date.now()}`,
  68. url: "https://api.anthropic.com",
  69. key: "sk-test-key-123456",
  70. provider_type: "claude",
  71. is_enabled: true,
  72. weight: 100,
  73. priority: 1,
  74. cost_multiplier: 1.0,
  75. });
  76. expect(response.ok).toBe(true);
  77. expect(data.ok).toBe(true);
  78. });
  79. test("应该成功添加 Codex 供应商", async () => {
  80. const { response, data } = await callProvidersApi("addProvider", {
  81. name: `测试供应商_Codex_${Date.now()}`,
  82. url: "https://api.openai.com",
  83. key: "sk-test-key-codex",
  84. provider_type: "codex",
  85. is_enabled: true,
  86. weight: 50,
  87. priority: 2,
  88. });
  89. expect(response.ok).toBe(true);
  90. expect(data.ok).toBe(true);
  91. });
  92. test("非管理员不能添加供应商", async () => {
  93. const { response, data } = await callProvidersApi(
  94. "addProvider",
  95. {
  96. name: "测试供应商",
  97. url: "https://api.example.com",
  98. key: "sk-test",
  99. provider_type: "claude",
  100. },
  101. USER_TOKEN
  102. );
  103. expect(response.ok).toBe(true);
  104. expect(data.ok).toBe(false);
  105. expect(data.error).toContain("无权限");
  106. });
  107. test("缺少必需参数应返回错误", async () => {
  108. const { response, data } = await callProvidersApi("addProvider", {
  109. name: "测试供应商",
  110. // 缺少 url 和 key
  111. provider_type: "claude",
  112. });
  113. expect(response.ok).toBe(true);
  114. expect(data.ok).toBe(false);
  115. expect(data.error).toBeDefined();
  116. });
  117. test("权重超出范围应返回验证失败", async () => {
  118. const { response, data } = await callProvidersApi("addProvider", {
  119. name: "测试供应商",
  120. url: "https://api.example.com",
  121. key: "sk-test",
  122. provider_type: "claude",
  123. weight: -1, // 应该 >= 0
  124. });
  125. expect(response.ok).toBe(true);
  126. expect(data.ok).toBe(false);
  127. expect(data.error).toBeDefined();
  128. });
  129. test("优先级超出范围应返回验证失败", async () => {
  130. const { response, data } = await callProvidersApi("addProvider", {
  131. name: "测试供应商",
  132. url: "https://api.example.com",
  133. key: "sk-test",
  134. provider_type: "claude",
  135. priority: 0, // 应该 >= 1
  136. });
  137. expect(response.ok).toBe(true);
  138. expect(data.ok).toBe(false);
  139. expect(data.error).toBeDefined();
  140. });
  141. test("添加带代理的供应商", async () => {
  142. const { response, data } = await callProvidersApi("addProvider", {
  143. name: `测试供应商_代理_${Date.now()}`,
  144. url: "https://api.anthropic.com",
  145. key: "sk-test-key",
  146. provider_type: "claude",
  147. proxy_url: "http://proxy.example.com:8080",
  148. proxy_fallback_to_direct: true,
  149. });
  150. expect(response.ok).toBe(true);
  151. expect(data.ok).toBe(true);
  152. });
  153. test("无效代理 URL 格式应返回错误", async () => {
  154. const { response, data } = await callProvidersApi("addProvider", {
  155. name: "测试供应商",
  156. url: "https://api.anthropic.com",
  157. key: "sk-test",
  158. provider_type: "claude",
  159. proxy_url: "invalid-proxy-url", // 无效格式
  160. });
  161. expect(response.ok).toBe(true);
  162. expect(data.ok).toBe(false);
  163. expect(data.error).toContain("代理地址格式无效");
  164. });
  165. test("添加带限额的供应商", async () => {
  166. const { response, data } = await callProvidersApi("addProvider", {
  167. name: `测试供应商_限额_${Date.now()}`,
  168. url: "https://api.anthropic.com",
  169. key: "sk-test-key",
  170. provider_type: "claude",
  171. limit_5h_usd: 10,
  172. limit_daily_usd: 50,
  173. limit_weekly_usd: 200,
  174. limit_monthly_usd: 500,
  175. limit_concurrent_sessions: 5,
  176. });
  177. expect(response.ok).toBe(true);
  178. expect(data.ok).toBe(true);
  179. });
  180. test("添加带熔断器配置的供应商", async () => {
  181. const { response, data } = await callProvidersApi("addProvider", {
  182. name: `测试供应商_熔断器_${Date.now()}`,
  183. url: "https://api.anthropic.com",
  184. key: "sk-test-key",
  185. provider_type: "claude",
  186. circuit_breaker_failure_threshold: 3,
  187. circuit_breaker_open_duration: 60000,
  188. circuit_breaker_half_open_success_threshold: 2,
  189. });
  190. expect(response.ok).toBe(true);
  191. expect(data.ok).toBe(true);
  192. });
  193. test("添加带模型重定向的供应商", async () => {
  194. const { response, data } = await callProvidersApi("addProvider", {
  195. name: `测试供应商_重定向_${Date.now()}`,
  196. url: "https://api.anthropic.com",
  197. key: "sk-test-key",
  198. provider_type: "claude",
  199. model_redirects: [
  200. {
  201. matchType: "prefix",
  202. source: "claude-3-opus",
  203. target: "claude-3-sonnet",
  204. },
  205. {
  206. matchType: "contains",
  207. source: "gpt-4",
  208. target: "claude-3-opus",
  209. },
  210. ],
  211. });
  212. expect(response.ok).toBe(true);
  213. expect(data.ok).toBe(true);
  214. });
  215. test("添加带分组标签的供应商", async () => {
  216. const { response, data } = await callProvidersApi("addProvider", {
  217. name: `测试供应商_分组_${Date.now()}`,
  218. url: "https://api.anthropic.com",
  219. key: "sk-test-key",
  220. provider_type: "claude",
  221. group_tag: "production",
  222. });
  223. expect(response.ok).toBe(true);
  224. expect(data.ok).toBe(true);
  225. });
  226. });
  227. describe.skip("供应商管理 - 编辑供应商 (editProvider)", () => {
  228. let testProviderId: number;
  229. beforeEach(async () => {
  230. // 创建测试供应商
  231. const { data: _data } = await callProvidersApi("addProvider", {
  232. name: `待编辑供应商_${Date.now()}`,
  233. url: "https://api.anthropic.com",
  234. key: "sk-test-key",
  235. provider_type: "claude",
  236. weight: 100,
  237. priority: 1,
  238. });
  239. // 获取创建的供应商 ID
  240. const providers = await callProvidersApi("getProviders");
  241. const createdProvider = providers.data.find(
  242. (p: any) => p.name === `待编辑供应商_${Date.now()}`
  243. );
  244. testProviderId = createdProvider?.id;
  245. });
  246. test("应该成功编辑供应商", async () => {
  247. if (!testProviderId) {
  248. console.log("跳过测试:无法创建测试供应商");
  249. return;
  250. }
  251. const { response, data } = await callProvidersApi("editProvider", {
  252. providerId: testProviderId,
  253. name: "已修改的供应商名",
  254. weight: 200,
  255. });
  256. expect(response.ok).toBe(true);
  257. expect(data.ok).toBe(true);
  258. });
  259. test("非管理员不能编辑供应商", async () => {
  260. if (!testProviderId) {
  261. console.log("跳过测试:无法创建测试供应商");
  262. return;
  263. }
  264. const { response, data } = await callProvidersApi(
  265. "editProvider",
  266. {
  267. providerId: testProviderId,
  268. name: "尝试修改",
  269. },
  270. USER_TOKEN
  271. );
  272. expect(response.ok).toBe(true);
  273. expect(data.ok).toBe(false);
  274. expect(data.error).toContain("无权限");
  275. });
  276. test("更新供应商权重", async () => {
  277. if (!testProviderId) {
  278. console.log("跳过测试:无法创建测试供应商");
  279. return;
  280. }
  281. const { response, data } = await callProvidersApi("editProvider", {
  282. providerId: testProviderId,
  283. weight: 150,
  284. });
  285. expect(response.ok).toBe(true);
  286. expect(data.ok).toBe(true);
  287. });
  288. test("更新供应商优先级", async () => {
  289. if (!testProviderId) {
  290. console.log("跳过测试:无法创建测试供应商");
  291. return;
  292. }
  293. const { response, data } = await callProvidersApi("editProvider", {
  294. providerId: testProviderId,
  295. priority: 5,
  296. });
  297. expect(response.ok).toBe(true);
  298. expect(data.ok).toBe(true);
  299. });
  300. test("更新供应商代理配置", async () => {
  301. if (!testProviderId) {
  302. console.log("跳过测试:无法创建测试供应商");
  303. return;
  304. }
  305. const { response, data } = await callProvidersApi("editProvider", {
  306. providerId: testProviderId,
  307. proxy_url: "http://proxy.example.com:3128",
  308. proxy_fallback_to_direct: true,
  309. });
  310. expect(response.ok).toBe(true);
  311. expect(data.ok).toBe(true);
  312. });
  313. test("更新供应商限额", async () => {
  314. if (!testProviderId) {
  315. console.log("跳过测试:无法创建测试供应商");
  316. return;
  317. }
  318. const { response, data } = await callProvidersApi("editProvider", {
  319. providerId: testProviderId,
  320. limit_5h_usd: 20,
  321. limit_daily_usd: 100,
  322. });
  323. expect(response.ok).toBe(true);
  324. expect(data.ok).toBe(true);
  325. });
  326. });
  327. describe.skip("供应商管理 - 删除供应商 (removeProvider)", () => {
  328. let testProviderId: number;
  329. beforeEach(async () => {
  330. // 创建待删除供应商
  331. await callProvidersApi("addProvider", {
  332. name: `待删除供应商_${Date.now()}`,
  333. url: "https://api.anthropic.com",
  334. key: "sk-test-key",
  335. provider_type: "claude",
  336. });
  337. const providers = await callProvidersApi("getProviders");
  338. const createdProvider = providers.data.find((p: any) => p.name.startsWith("待删除供应商_"));
  339. testProviderId = createdProvider?.id;
  340. });
  341. test("应该成功删除供应商", async () => {
  342. if (!testProviderId) {
  343. console.log("跳过测试:无法创建测试供应商");
  344. return;
  345. }
  346. const { response, data } = await callProvidersApi("removeProvider", {
  347. providerId: testProviderId,
  348. });
  349. expect(response.ok).toBe(true);
  350. expect(data.ok).toBe(true);
  351. });
  352. test("非管理员不能删除供应商", async () => {
  353. if (!testProviderId) {
  354. console.log("跳过测试:无法创建测试供应商");
  355. return;
  356. }
  357. const { response, data } = await callProvidersApi(
  358. "removeProvider",
  359. {
  360. providerId: testProviderId,
  361. },
  362. USER_TOKEN
  363. );
  364. expect(response.ok).toBe(true);
  365. expect(data.ok).toBe(false);
  366. expect(data.error).toContain("无权限");
  367. });
  368. test("删除不存在的供应商应返回错误", async () => {
  369. const { response, data } = await callProvidersApi("removeProvider", {
  370. providerId: 999999,
  371. });
  372. expect(response.ok).toBe(true);
  373. expect(data.ok).toBe(false);
  374. });
  375. });
  376. describe.skip("供应商管理 - 熔断器健康状态 (getProvidersHealthStatus)", () => {
  377. test("应该成功获取熔断器状态", async () => {
  378. const { response, data } = await callProvidersApi("getProvidersHealthStatus");
  379. expect(response.ok).toBe(true);
  380. expect(typeof data).toBe("object");
  381. // 熔断器状态应该是一个对象,键为供应商 ID
  382. });
  383. test("非管理员不能查看熔断器状态", async () => {
  384. const { response, data } = await callProvidersApi("getProvidersHealthStatus", {}, USER_TOKEN);
  385. expect(response.ok).toBe(true);
  386. expect(typeof data).toBe("object");
  387. expect(Object.keys(data).length).toBe(0);
  388. });
  389. });
  390. describe.skip("供应商管理 - 重置熔断器 (resetProviderCircuit)", () => {
  391. let testProviderId: number;
  392. beforeEach(async () => {
  393. // 创建测试供应商
  394. await callProvidersApi("addProvider", {
  395. name: `熔断器测试供应商_${Date.now()}`,
  396. url: "https://api.anthropic.com",
  397. key: "sk-test-key",
  398. provider_type: "claude",
  399. });
  400. const providers = await callProvidersApi("getProviders");
  401. const createdProvider = providers.data.find((p: any) => p.name.startsWith("熔断器测试供应商_"));
  402. testProviderId = createdProvider?.id;
  403. });
  404. test("应该成功重置熔断器", async () => {
  405. if (!testProviderId) {
  406. console.log("跳过测试:无法创建测试供应商");
  407. return;
  408. }
  409. const { response, data } = await callProvidersApi("resetProviderCircuit", {
  410. providerId: testProviderId,
  411. });
  412. expect(response.ok).toBe(true);
  413. expect(data.ok).toBe(true);
  414. });
  415. test("非管理员不能重置熔断器", async () => {
  416. if (!testProviderId) {
  417. console.log("跳过测试:无法创建测试供应商");
  418. return;
  419. }
  420. const { response, data } = await callProvidersApi(
  421. "resetProviderCircuit",
  422. {
  423. providerId: testProviderId,
  424. },
  425. USER_TOKEN
  426. );
  427. expect(response.ok).toBe(true);
  428. expect(data.ok).toBe(false);
  429. expect(data.error).toContain("无权限");
  430. });
  431. });
  432. describe.skip("供应商管理 - 获取供应商限额使用情况 (getProviderLimitUsage)", () => {
  433. let testProviderId: number;
  434. beforeEach(async () => {
  435. // 创建带限额的测试供应商
  436. await callProvidersApi("addProvider", {
  437. name: `限额测试供应商_${Date.now()}`,
  438. url: "https://api.anthropic.com",
  439. key: "sk-test-key",
  440. provider_type: "claude",
  441. limit_5h_usd: 10,
  442. limit_daily_usd: 50,
  443. limit_weekly_usd: 200,
  444. limit_monthly_usd: 500,
  445. });
  446. const providers = await callProvidersApi("getProviders");
  447. const createdProvider = providers.data.find((p: any) => p.name.startsWith("限额测试供应商_"));
  448. testProviderId = createdProvider?.id;
  449. });
  450. test("应该成功获取供应商限额使用情况", async () => {
  451. if (!testProviderId) {
  452. console.log("跳过测试:无法创建测试供应商");
  453. return;
  454. }
  455. const { response, data } = await callProvidersApi("getProviderLimitUsage", {
  456. providerId: testProviderId,
  457. });
  458. expect(response.ok).toBe(true);
  459. expect(data.ok).toBe(true);
  460. expect(data.data).toBeDefined();
  461. expect(data.data.cost5h).toBeDefined();
  462. expect(data.data.costDaily).toBeDefined();
  463. expect(data.data.costWeekly).toBeDefined();
  464. expect(data.data.costMonthly).toBeDefined();
  465. expect(data.data.concurrentSessions).toBeDefined();
  466. });
  467. test("非管理员不能查看供应商限额", async () => {
  468. if (!testProviderId) {
  469. console.log("跳过测试:无法创建测试供应商");
  470. return;
  471. }
  472. const { response, data } = await callProvidersApi(
  473. "getProviderLimitUsage",
  474. {
  475. providerId: testProviderId,
  476. },
  477. USER_TOKEN
  478. );
  479. expect(response.ok).toBe(true);
  480. expect(data.ok).toBe(false);
  481. expect(data.error).toContain("无权限");
  482. });
  483. });
  484. describe.skip("供应商管理 - 测试代理连接 (testProviderProxy)", () => {
  485. test("应该成功测试无代理连接", async () => {
  486. const { response, data } = await callProvidersApi("testProviderProxy", {
  487. providerUrl: "https://api.anthropic.com",
  488. proxyUrl: null,
  489. });
  490. expect(response.ok).toBe(true);
  491. expect(data.ok).toBe(true);
  492. expect(data.data).toBeDefined();
  493. expect(data.data.success).toBeDefined();
  494. expect(data.data.message).toBeDefined();
  495. });
  496. test("无效的代理 URL 应返回错误", async () => {
  497. const { response, data } = await callProvidersApi("testProviderProxy", {
  498. providerUrl: "https://api.anthropic.com",
  499. proxyUrl: "invalid-proxy",
  500. });
  501. expect(response.ok).toBe(true);
  502. expect(data.ok).toBe(true);
  503. expect(data.data.success).toBe(false);
  504. expect(data.data.message).toContain("代理地址格式无效");
  505. });
  506. test("非管理员不能测试代理连接", async () => {
  507. const { response, data } = await callProvidersApi(
  508. "testProviderProxy",
  509. {
  510. providerUrl: "https://api.anthropic.com",
  511. },
  512. USER_TOKEN
  513. );
  514. expect(response.ok).toBe(true);
  515. expect(data.ok).toBe(false);
  516. expect(data.error).toContain("无权限");
  517. });
  518. });
  519. describe.skip("供应商管理 - 获取完整密钥 (getUnmaskedProviderKey)", () => {
  520. let testProviderId: number;
  521. beforeEach(async () => {
  522. // 创建测试供应商
  523. await callProvidersApi("addProvider", {
  524. name: `密钥测试供应商_${Date.now()}`,
  525. url: "https://api.anthropic.com",
  526. key: "sk-test-complete-key-123456",
  527. provider_type: "claude",
  528. });
  529. const providers = await callProvidersApi("getProviders");
  530. const createdProvider = providers.data.find((p: any) => p.name.startsWith("密钥测试供应商_"));
  531. testProviderId = createdProvider?.id;
  532. });
  533. test("管理员应该可以获取完整密钥", async () => {
  534. if (!testProviderId) {
  535. console.log("跳过测试:无法创建测试供应商");
  536. return;
  537. }
  538. const { response, data } = await callProvidersApi("getUnmaskedProviderKey", {
  539. id: testProviderId,
  540. });
  541. expect(response.ok).toBe(true);
  542. expect(data.ok).toBe(true);
  543. expect(data.data).toBeDefined();
  544. expect(data.data.key).toBeDefined();
  545. expect(data.data.key).toMatch(/^sk-/);
  546. });
  547. test("非管理员不能获取完整密钥", async () => {
  548. if (!testProviderId) {
  549. console.log("跳过测试:无法创建测试供应商");
  550. return;
  551. }
  552. const { response, data } = await callProvidersApi(
  553. "getUnmaskedProviderKey",
  554. {
  555. id: testProviderId,
  556. },
  557. USER_TOKEN
  558. );
  559. expect(response.ok).toBe(true);
  560. expect(data.ok).toBe(false);
  561. expect(data.error).toContain("权限不足");
  562. });
  563. test("获取不存在的供应商密钥应返回错误", async () => {
  564. const { response, data } = await callProvidersApi("getUnmaskedProviderKey", {
  565. id: 999999,
  566. });
  567. expect(response.ok).toBe(true);
  568. expect(data.ok).toBe(false);
  569. expect(data.error).toContain("供应商不存在");
  570. });
  571. });
  572. describe.skip("供应商管理 - 响应格式验证", () => {
  573. test("所有成功响应应符合 ActionResult 格式", async () => {
  574. const { response, data } = await callProvidersApi("getProviders");
  575. expect(response.ok).toBe(true);
  576. expect(Array.isArray(data)).toBe(true);
  577. });
  578. test("所有错误响应应符合 ActionResult 格式", async () => {
  579. const { response, data } = await callProvidersApi(
  580. "addProvider",
  581. {
  582. name: "测试供应商",
  583. url: "https://api.example.com",
  584. key: "sk-test",
  585. provider_type: "claude",
  586. },
  587. USER_TOKEN
  588. );
  589. expect(response.ok).toBe(true);
  590. expect(data).toHaveProperty("ok");
  591. expect(data.ok).toBe(false);
  592. expect(data).toHaveProperty("error");
  593. expect(typeof data.error).toBe("string");
  594. });
  595. });