provider-endpoints.test.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. const getSessionMock = vi.fn();
  3. const updateProviderVendorMock = vi.fn();
  4. const deleteProviderVendorMock = vi.fn();
  5. const publishProviderCacheInvalidationMock = vi.fn();
  6. const findProviderEndpointByIdMock = vi.fn();
  7. const softDeleteProviderEndpointMock = vi.fn();
  8. const tryDeleteProviderVendorIfEmptyMock = vi.fn();
  9. const updateProviderEndpointMock = vi.fn();
  10. const findProviderEndpointProbeLogsBatchMock = vi.fn();
  11. const findVendorTypeEndpointStatsBatchMock = vi.fn();
  12. const findEnabledProviderReferencesForVendorTypeUrlMock = vi.fn();
  13. const findEnabledProviderIdsByVendorAndTypeMock = vi.fn();
  14. const findDashboardProviderEndpointsByVendorAndTypeMock = vi.fn();
  15. const terminateProviderSessionsBatchMock = vi.fn();
  16. vi.mock("@/lib/auth", () => ({
  17. getSession: getSessionMock,
  18. }));
  19. vi.mock("@/lib/cache/provider-cache", () => ({
  20. publishProviderCacheInvalidation: publishProviderCacheInvalidationMock,
  21. }));
  22. vi.mock("@/lib/logger", () => ({
  23. logger: {
  24. trace: vi.fn(),
  25. debug: vi.fn(),
  26. info: vi.fn(),
  27. warn: vi.fn(),
  28. error: vi.fn(),
  29. },
  30. }));
  31. vi.mock("@/lib/endpoint-circuit-breaker", () => ({
  32. getAllEndpointHealthStatusAsync: vi.fn(async () => ({})),
  33. getEndpointHealthInfo: vi.fn(async () => ({ health: {}, config: {} })),
  34. resetEndpointCircuit: vi.fn(async () => {}),
  35. }));
  36. vi.mock("@/lib/vendor-type-circuit-breaker", () => ({
  37. getVendorTypeCircuitInfo: vi.fn(async () => ({
  38. vendorId: 1,
  39. providerType: "claude",
  40. circuitState: "closed",
  41. circuitOpenUntil: null,
  42. lastFailureTime: null,
  43. manualOpen: false,
  44. })),
  45. resetVendorTypeCircuit: vi.fn(async () => {}),
  46. setVendorTypeCircuitManualOpen: vi.fn(async () => {}),
  47. }));
  48. vi.mock("@/lib/provider-endpoints/probe", () => ({
  49. probeProviderEndpointAndRecordByEndpoint: vi.fn(async () => null),
  50. }));
  51. vi.mock("@/lib/session-manager", () => ({
  52. SessionManager: {
  53. terminateProviderSessionsBatch: terminateProviderSessionsBatchMock,
  54. terminateStickySessionsForProviders: terminateProviderSessionsBatchMock,
  55. },
  56. }));
  57. vi.mock("@/repository/provider-endpoints-batch", () => ({
  58. findProviderEndpointProbeLogsBatch: findProviderEndpointProbeLogsBatchMock,
  59. findVendorTypeEndpointStatsBatch: findVendorTypeEndpointStatsBatchMock,
  60. }));
  61. vi.mock("@/repository/provider-endpoints", () => ({
  62. findDashboardProviderEndpointsByVendorAndType: findDashboardProviderEndpointsByVendorAndTypeMock,
  63. findEnabledProviderVendorTypePairs: vi.fn(async () => []),
  64. findEnabledProviderReferencesForVendorTypeUrl: findEnabledProviderReferencesForVendorTypeUrlMock,
  65. findEnabledProviderIdsByVendorAndType: findEnabledProviderIdsByVendorAndTypeMock,
  66. }));
  67. vi.mock("@/repository", () => ({
  68. createProviderEndpoint: vi.fn(async () => ({})),
  69. deleteProviderVendor: deleteProviderVendorMock,
  70. findProviderEndpointById: findProviderEndpointByIdMock,
  71. findProviderEndpointProbeLogs: vi.fn(async () => []),
  72. findProviderEndpointsByVendorAndType: vi.fn(async () => []),
  73. findProviderVendorById: vi.fn(async () => null),
  74. findProviderVendors: vi.fn(async () => []),
  75. softDeleteProviderEndpoint: softDeleteProviderEndpointMock,
  76. tryDeleteProviderVendorIfEmpty: tryDeleteProviderVendorIfEmptyMock,
  77. updateProviderEndpoint: updateProviderEndpointMock,
  78. updateProviderVendor: updateProviderVendorMock,
  79. }));
  80. describe("provider-endpoints actions", () => {
  81. beforeEach(() => {
  82. vi.clearAllMocks();
  83. findEnabledProviderReferencesForVendorTypeUrlMock.mockResolvedValue([]);
  84. findEnabledProviderIdsByVendorAndTypeMock.mockResolvedValue([]);
  85. findDashboardProviderEndpointsByVendorAndTypeMock.mockResolvedValue([]);
  86. terminateProviderSessionsBatchMock.mockResolvedValue(0);
  87. });
  88. it("editProviderVendor: requires admin", async () => {
  89. getSessionMock.mockResolvedValue({ user: { role: "user" } });
  90. const { editProviderVendor } = await import("@/actions/provider-endpoints");
  91. const res = await editProviderVendor({ vendorId: 1, displayName: "x" });
  92. expect(res.ok).toBe(false);
  93. expect(res.errorCode).toBe("PERMISSION_DENIED");
  94. });
  95. it("getDashboardProviderEndpoints: requires admin", async () => {
  96. getSessionMock.mockResolvedValue({ user: { role: "user" } });
  97. const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
  98. const res = await getDashboardProviderEndpoints({ vendorId: 1, providerType: "claude" });
  99. expect(res).toEqual([]);
  100. expect(findDashboardProviderEndpointsByVendorAndTypeMock).not.toHaveBeenCalled();
  101. });
  102. it("getDashboardProviderEndpoints: invalid input returns empty list", async () => {
  103. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  104. const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
  105. const res = await getDashboardProviderEndpoints({ vendorId: 0, providerType: "claude" });
  106. expect(res).toEqual([]);
  107. expect(findDashboardProviderEndpointsByVendorAndTypeMock).not.toHaveBeenCalled();
  108. });
  109. it("getDashboardProviderEndpoints: returns endpoints in use for enabled providers", async () => {
  110. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  111. const endpoints = [
  112. {
  113. id: 1,
  114. vendorId: 10,
  115. providerType: "claude",
  116. url: "https://api.example.com",
  117. label: null,
  118. sortOrder: 0,
  119. isEnabled: true,
  120. lastProbedAt: null,
  121. lastProbeOk: null,
  122. lastProbeStatusCode: null,
  123. lastProbeLatencyMs: null,
  124. lastProbeErrorType: null,
  125. lastProbeErrorMessage: null,
  126. createdAt: new Date(),
  127. updatedAt: new Date(),
  128. deletedAt: null,
  129. },
  130. ];
  131. findDashboardProviderEndpointsByVendorAndTypeMock.mockResolvedValue(endpoints);
  132. const { getDashboardProviderEndpoints } = await import("@/actions/provider-endpoints");
  133. const res = await getDashboardProviderEndpoints({ vendorId: 10, providerType: "claude" });
  134. expect(res).toEqual(endpoints);
  135. expect(findDashboardProviderEndpointsByVendorAndTypeMock).toHaveBeenCalledWith(10, "claude");
  136. });
  137. it("editProviderVendor: computes favicon", async () => {
  138. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  139. updateProviderVendorMock.mockResolvedValue({
  140. id: 1,
  141. websiteDomain: "example.com",
  142. displayName: "Example",
  143. websiteUrl: "https://example.com/path",
  144. faviconUrl: "https://www.google.com/s2/favicons?domain=example.com&sz=32",
  145. createdAt: new Date(),
  146. updatedAt: new Date(),
  147. });
  148. const { editProviderVendor } = await import("@/actions/provider-endpoints");
  149. const res = await editProviderVendor({
  150. vendorId: 1,
  151. displayName: "Example",
  152. websiteUrl: "https://example.com/path",
  153. });
  154. expect(res.ok).toBe(true);
  155. expect(updateProviderVendorMock).toHaveBeenCalledWith(
  156. 1,
  157. expect.objectContaining({
  158. displayName: "Example",
  159. websiteUrl: "https://example.com/path",
  160. faviconUrl: "https://www.google.com/s2/favicons?domain=example.com&sz=32",
  161. })
  162. );
  163. });
  164. it("editProviderVendor: clearing websiteUrl clears faviconUrl", async () => {
  165. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  166. updateProviderVendorMock.mockResolvedValue({
  167. id: 1,
  168. websiteDomain: "example.com",
  169. displayName: null,
  170. websiteUrl: null,
  171. faviconUrl: null,
  172. createdAt: new Date(),
  173. updatedAt: new Date(),
  174. });
  175. const { editProviderVendor } = await import("@/actions/provider-endpoints");
  176. const res = await editProviderVendor({
  177. vendorId: 1,
  178. websiteUrl: null,
  179. });
  180. expect(res.ok).toBe(true);
  181. expect(updateProviderVendorMock).toHaveBeenCalledWith(
  182. 1,
  183. expect.objectContaining({
  184. websiteUrl: null,
  185. faviconUrl: null,
  186. })
  187. );
  188. });
  189. it("editProviderEndpoint: conflict maps to CONFLICT errorCode", async () => {
  190. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  191. findProviderEndpointByIdMock.mockResolvedValue({
  192. id: 42,
  193. vendorId: 123,
  194. providerType: "claude",
  195. url: "https://api.example.com",
  196. label: null,
  197. sortOrder: 0,
  198. isEnabled: true,
  199. lastProbedAt: null,
  200. lastProbeOk: null,
  201. lastProbeStatusCode: null,
  202. lastProbeLatencyMs: null,
  203. lastProbeErrorType: null,
  204. lastProbeErrorMessage: null,
  205. createdAt: new Date(),
  206. updatedAt: new Date(),
  207. deletedAt: null,
  208. });
  209. updateProviderEndpointMock.mockRejectedValue(
  210. Object.assign(new Error("[ProviderEndpointEdit] endpoint conflict"), {
  211. code: "PROVIDER_ENDPOINT_CONFLICT",
  212. })
  213. );
  214. const { editProviderEndpoint } = await import("@/actions/provider-endpoints");
  215. const res = await editProviderEndpoint({
  216. endpointId: 42,
  217. url: "https://next.example.com/v1/messages",
  218. });
  219. expect(res.ok).toBe(false);
  220. expect(res.errorCode).toBe("CONFLICT");
  221. expect(res.error).not.toContain("duplicate key value");
  222. });
  223. it("editProviderEndpoint: success returns ok with endpoint payload", async () => {
  224. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  225. const endpoint = {
  226. id: 42,
  227. vendorId: 123,
  228. providerType: "claude" as const,
  229. url: "https://next.example.com/v1/messages",
  230. label: "primary",
  231. sortOrder: 7,
  232. isEnabled: false,
  233. lastProbedAt: null,
  234. lastProbeOk: null,
  235. lastProbeStatusCode: null,
  236. lastProbeLatencyMs: null,
  237. lastProbeErrorType: null,
  238. lastProbeErrorMessage: null,
  239. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  240. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  241. deletedAt: null,
  242. };
  243. findProviderEndpointByIdMock.mockResolvedValue(endpoint);
  244. updateProviderEndpointMock.mockResolvedValue(endpoint);
  245. const { editProviderEndpoint } = await import("@/actions/provider-endpoints");
  246. const res = await editProviderEndpoint({
  247. endpointId: 42,
  248. url: endpoint.url,
  249. label: endpoint.label,
  250. sortOrder: endpoint.sortOrder,
  251. isEnabled: endpoint.isEnabled,
  252. });
  253. expect(res.ok).toBe(true);
  254. expect(res.data?.endpoint).toEqual(endpoint);
  255. expect(updateProviderEndpointMock).toHaveBeenCalledWith(42, {
  256. url: endpoint.url,
  257. label: endpoint.label,
  258. sortOrder: endpoint.sortOrder,
  259. isEnabled: endpoint.isEnabled,
  260. });
  261. });
  262. it("removeProviderVendor: deletes vendor and publishes cache invalidation", async () => {
  263. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  264. deleteProviderVendorMock.mockResolvedValue(true);
  265. publishProviderCacheInvalidationMock.mockResolvedValue(undefined);
  266. const { removeProviderVendor } = await import("@/actions/provider-endpoints");
  267. const res = await removeProviderVendor({ vendorId: 1 });
  268. expect(res.ok).toBe(true);
  269. expect(deleteProviderVendorMock).toHaveBeenCalledWith(1);
  270. expect(publishProviderCacheInvalidationMock).toHaveBeenCalledTimes(1);
  271. });
  272. it("removeProviderVendor: still ok when cache invalidation fails", async () => {
  273. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  274. deleteProviderVendorMock.mockResolvedValue(true);
  275. publishProviderCacheInvalidationMock.mockRejectedValue(new Error("boom"));
  276. const { removeProviderVendor } = await import("@/actions/provider-endpoints");
  277. const res = await removeProviderVendor({ vendorId: 1 });
  278. expect(res.ok).toBe(true);
  279. expect(deleteProviderVendorMock).toHaveBeenCalledWith(1);
  280. });
  281. it("removeProviderEndpoint: triggers vendor cleanup after soft delete", async () => {
  282. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  283. findProviderEndpointByIdMock.mockResolvedValue({
  284. id: 99,
  285. vendorId: 123,
  286. providerType: "claude",
  287. url: "https://api.example.com",
  288. label: null,
  289. sortOrder: 0,
  290. isEnabled: true,
  291. lastProbedAt: null,
  292. lastProbeOk: null,
  293. lastProbeStatusCode: null,
  294. lastProbeLatencyMs: null,
  295. lastProbeErrorType: null,
  296. lastProbeErrorMessage: null,
  297. createdAt: new Date(),
  298. updatedAt: new Date(),
  299. deletedAt: null,
  300. });
  301. softDeleteProviderEndpointMock.mockResolvedValue(true);
  302. tryDeleteProviderVendorIfEmptyMock.mockResolvedValue(true);
  303. findEnabledProviderIdsByVendorAndTypeMock.mockResolvedValue([11, 12]);
  304. const { removeProviderEndpoint } = await import("@/actions/provider-endpoints");
  305. const res = await removeProviderEndpoint({ endpointId: 99 });
  306. expect(res.ok).toBe(true);
  307. const { resetEndpointCircuit } = await import("@/lib/endpoint-circuit-breaker");
  308. expect(resetEndpointCircuit).toHaveBeenCalledWith(99);
  309. expect(tryDeleteProviderVendorIfEmptyMock).toHaveBeenCalledWith(123);
  310. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith(
  311. [11, 12],
  312. "removeProviderEndpoint"
  313. );
  314. });
  315. it("removeProviderEndpoint: returns detailed conflict when endpoint is still referenced", async () => {
  316. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  317. findProviderEndpointByIdMock.mockResolvedValue({
  318. id: 99,
  319. vendorId: 123,
  320. providerType: "claude",
  321. url: "https://api.example.com",
  322. label: null,
  323. sortOrder: 0,
  324. isEnabled: true,
  325. lastProbedAt: null,
  326. lastProbeOk: null,
  327. lastProbeStatusCode: null,
  328. lastProbeLatencyMs: null,
  329. lastProbeErrorType: null,
  330. lastProbeErrorMessage: null,
  331. createdAt: new Date(),
  332. updatedAt: new Date(),
  333. deletedAt: null,
  334. });
  335. findEnabledProviderReferencesForVendorTypeUrlMock.mockResolvedValue([
  336. { id: 1, name: "CPA Primary" },
  337. { id: 2, name: "CPA Backup" },
  338. { id: 3, name: "CPA Canary" },
  339. { id: 4, name: "CPA Extra" },
  340. ]);
  341. const { removeProviderEndpoint } = await import("@/actions/provider-endpoints");
  342. const res = await removeProviderEndpoint({ endpointId: 99 });
  343. expect(res.ok).toBe(false);
  344. expect(res.errorCode).toBe("ENDPOINT_REFERENCED_BY_ENABLED_PROVIDERS");
  345. expect(res.errorParams).toEqual({
  346. count: 4,
  347. providers: "CPA Primary, CPA Backup, CPA Canary +1",
  348. });
  349. expect(softDeleteProviderEndpointMock).not.toHaveBeenCalled();
  350. expect(terminateProviderSessionsBatchMock).not.toHaveBeenCalled();
  351. });
  352. it("editProviderEndpoint: route-affecting changes terminate affected provider sessions", async () => {
  353. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  354. const endpoint = {
  355. id: 42,
  356. vendorId: 123,
  357. providerType: "claude" as const,
  358. url: "https://next.example.com/v1/messages",
  359. label: "primary",
  360. sortOrder: 7,
  361. isEnabled: true,
  362. lastProbedAt: null,
  363. lastProbeOk: null,
  364. lastProbeStatusCode: null,
  365. lastProbeLatencyMs: null,
  366. lastProbeErrorType: null,
  367. lastProbeErrorMessage: null,
  368. createdAt: new Date("2026-01-01T00:00:00.000Z"),
  369. updatedAt: new Date("2026-01-01T00:00:00.000Z"),
  370. deletedAt: null,
  371. };
  372. findProviderEndpointByIdMock.mockResolvedValue(endpoint);
  373. updateProviderEndpointMock.mockResolvedValue({
  374. ...endpoint,
  375. isEnabled: false,
  376. });
  377. findEnabledProviderIdsByVendorAndTypeMock.mockResolvedValue([7, 8]);
  378. const { editProviderEndpoint } = await import("@/actions/provider-endpoints");
  379. const res = await editProviderEndpoint({
  380. endpointId: 42,
  381. isEnabled: false,
  382. });
  383. expect(res.ok).toBe(true);
  384. expect(terminateProviderSessionsBatchMock).toHaveBeenCalledWith([7, 8], "editProviderEndpoint");
  385. });
  386. it("probeProviderEndpoint: calls probeProviderEndpointAndRecordByEndpoint and returns result", async () => {
  387. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  388. const endpoint = {
  389. id: 7,
  390. vendorId: 123,
  391. providerType: "claude",
  392. url: "https://api.example.com",
  393. label: null,
  394. sortOrder: 0,
  395. isEnabled: true,
  396. lastProbedAt: null,
  397. lastProbeOk: null,
  398. lastProbeStatusCode: null,
  399. lastProbeLatencyMs: null,
  400. lastProbeErrorType: null,
  401. lastProbeErrorMessage: null,
  402. createdAt: new Date(),
  403. updatedAt: new Date(),
  404. deletedAt: null,
  405. };
  406. findProviderEndpointByIdMock.mockResolvedValue(endpoint);
  407. const { probeProviderEndpointAndRecordByEndpoint } = await import(
  408. "@/lib/provider-endpoints/probe"
  409. );
  410. const result = {
  411. ok: true,
  412. method: "HEAD",
  413. statusCode: 200,
  414. latencyMs: 10,
  415. errorType: null,
  416. errorMessage: null,
  417. } as const;
  418. vi.mocked(probeProviderEndpointAndRecordByEndpoint).mockResolvedValue(result);
  419. const { probeProviderEndpoint } = await import("@/actions/provider-endpoints");
  420. const res = await probeProviderEndpoint({ endpointId: 7, timeoutMs: 5000 });
  421. expect(res.ok).toBe(true);
  422. expect(probeProviderEndpointAndRecordByEndpoint).toHaveBeenCalledWith({
  423. endpoint,
  424. source: "manual",
  425. timeoutMs: 5000,
  426. });
  427. expect(res.data?.result).toEqual(result);
  428. });
  429. describe("batchGetEndpointCircuitInfo", () => {
  430. it("returns circuit info for multiple endpoints", async () => {
  431. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  432. const { getAllEndpointHealthStatusAsync } = await import("@/lib/endpoint-circuit-breaker");
  433. vi.mocked(getAllEndpointHealthStatusAsync).mockResolvedValue({
  434. 1: {
  435. failureCount: 0,
  436. lastFailureTime: null,
  437. circuitState: "closed",
  438. circuitOpenUntil: null,
  439. halfOpenSuccessCount: 0,
  440. },
  441. 2: {
  442. failureCount: 5,
  443. lastFailureTime: Date.now(),
  444. circuitState: "open",
  445. circuitOpenUntil: Date.now() + 60000,
  446. halfOpenSuccessCount: 0,
  447. },
  448. 3: {
  449. failureCount: 1,
  450. lastFailureTime: Date.now() - 1000,
  451. circuitState: "half-open",
  452. circuitOpenUntil: null,
  453. halfOpenSuccessCount: 0,
  454. },
  455. });
  456. const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
  457. const res = await batchGetEndpointCircuitInfo({ endpointIds: [1, 2, 3] });
  458. expect(res.ok).toBe(true);
  459. expect(getAllEndpointHealthStatusAsync).toHaveBeenCalledWith([1, 2, 3]);
  460. expect(res.data).toHaveLength(3);
  461. expect(res.data?.[0]).toEqual({
  462. endpointId: 1,
  463. circuitState: "closed",
  464. failureCount: 0,
  465. circuitOpenUntil: null,
  466. });
  467. expect(res.data?.[1]).toEqual({
  468. endpointId: 2,
  469. circuitState: "open",
  470. failureCount: 5,
  471. circuitOpenUntil: expect.any(Number),
  472. });
  473. expect(res.data?.[2]).toEqual({
  474. endpointId: 3,
  475. circuitState: "half-open",
  476. failureCount: 1,
  477. circuitOpenUntil: null,
  478. });
  479. });
  480. it("returns empty array for empty input", async () => {
  481. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  482. const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
  483. const res = await batchGetEndpointCircuitInfo({ endpointIds: [] });
  484. expect(res.ok).toBe(true);
  485. expect(res.data).toEqual([]);
  486. });
  487. it("requires admin session", async () => {
  488. getSessionMock.mockResolvedValue({ user: { role: "user" } });
  489. const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
  490. const res = await batchGetEndpointCircuitInfo({ endpointIds: [1, 2] });
  491. expect(res.ok).toBe(false);
  492. expect(res.errorCode).toBe("PERMISSION_DENIED");
  493. });
  494. it("validates endpointIds are positive integers", async () => {
  495. getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  496. const { batchGetEndpointCircuitInfo } = await import("@/actions/provider-endpoints");
  497. const res = await batchGetEndpointCircuitInfo({ endpointIds: [0, -1, 1] });
  498. expect(res.ok).toBe(false);
  499. expect(res.errorCode).toBe("MIN_VALUE");
  500. });
  501. });
  502. });