providers-api-test.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. const getSessionMock = vi.fn();
  3. const executeProviderTestMock = vi.fn();
  4. const getPresetsForProviderMock = vi.fn();
  5. const validateProviderUrlForConnectivityMock = vi.fn();
  6. const createProxyAgentForProviderMock = vi.fn();
  7. const getAccessTokenMock = vi.fn();
  8. const isJsonMock = vi.fn();
  9. vi.mock("@/lib/auth", () => ({
  10. getSession: getSessionMock,
  11. }));
  12. vi.mock("@/repository/provider", () => ({
  13. createProvider: vi.fn(),
  14. deleteProvider: vi.fn(),
  15. findAllProviders: vi.fn(async () => []),
  16. findAllProvidersFresh: vi.fn(async () => []),
  17. findProviderById: vi.fn(),
  18. getProviderStatistics: vi.fn(),
  19. resetProviderTotalCostResetAt: vi.fn(async () => {}),
  20. updateProvider: vi.fn(),
  21. updateProviderPrioritiesBatch: vi.fn(),
  22. }));
  23. vi.mock("@/lib/cache/provider-cache", () => ({
  24. publishProviderCacheInvalidation: vi.fn(),
  25. }));
  26. vi.mock("@/lib/redis/circuit-breaker-config", () => ({
  27. deleteProviderCircuitConfig: vi.fn(),
  28. saveProviderCircuitConfig: vi.fn(),
  29. }));
  30. vi.mock("@/lib/circuit-breaker", () => ({
  31. clearConfigCache: vi.fn(),
  32. clearProviderState: vi.fn(),
  33. getAllHealthStatusAsync: vi.fn(async () => ({})),
  34. publishCircuitBreakerConfigInvalidation: vi.fn(),
  35. forceCloseCircuitState: vi.fn(),
  36. resetCircuit: vi.fn(),
  37. }));
  38. vi.mock("@/lib/session-manager", () => ({
  39. SessionManager: {
  40. terminateProviderSessionsBatch: vi.fn(),
  41. terminateStickySessionsForProviders: vi.fn(),
  42. },
  43. }));
  44. vi.mock("@/lib/logger", () => ({
  45. logger: {
  46. trace: vi.fn(),
  47. debug: vi.fn(),
  48. info: vi.fn(),
  49. warn: vi.fn(),
  50. error: vi.fn(),
  51. },
  52. }));
  53. vi.mock("next/cache", () => ({
  54. revalidatePath: vi.fn(),
  55. }));
  56. vi.mock("@/lib/provider-testing", () => ({
  57. executeProviderTest: executeProviderTestMock,
  58. }));
  59. vi.mock("@/lib/provider-testing/presets", () => ({
  60. getPresetsForProvider: getPresetsForProviderMock,
  61. }));
  62. vi.mock("@/lib/validation/provider-url", () => ({
  63. validateProviderUrlForConnectivity: validateProviderUrlForConnectivityMock,
  64. }));
  65. vi.mock("@/lib/proxy-agent", () => ({
  66. createProxyAgentForProvider: createProxyAgentForProviderMock,
  67. isValidProxyUrl: vi.fn(() => true),
  68. }));
  69. vi.mock("@/app/v1/_lib/gemini/auth", () => ({
  70. GeminiAuth: {
  71. getAccessToken: getAccessTokenMock,
  72. isJson: isJsonMock,
  73. },
  74. }));
  75. const fetchMock = vi.fn<typeof fetch>();
  76. describe("providers api test actions", () => {
  77. beforeEach(() => {
  78. vi.clearAllMocks();
  79. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  80. validateProviderUrlForConnectivityMock.mockImplementation((providerUrl: string) => ({
  81. valid: true,
  82. normalizedUrl: providerUrl,
  83. }));
  84. createProxyAgentForProviderMock.mockReturnValue(null);
  85. getAccessTokenMock.mockImplementation(async (apiKey: string) => apiKey);
  86. isJsonMock.mockReturnValue(false);
  87. getPresetsForProviderMock.mockReturnValue([]);
  88. global.fetch = fetchMock as typeof fetch;
  89. });
  90. test("testProviderUnified 应该把 executeProviderTest 返回的完整 rawResponse 透传给前端", async () => {
  91. executeProviderTestMock.mockResolvedValue({
  92. success: true,
  93. status: "green",
  94. subStatus: "success",
  95. message: "ok",
  96. latencyMs: 123,
  97. firstByteMs: 45,
  98. httpStatusCode: 200,
  99. httpStatusText: "OK",
  100. model: "gpt-4.1-mini",
  101. content: "pong",
  102. rawResponse: '{"message":"pong"}',
  103. usage: undefined,
  104. streamInfo: undefined,
  105. errorMessage: undefined,
  106. errorType: undefined,
  107. testedAt: new Date("2026-04-08T00:00:00.000Z"),
  108. validationDetails: {
  109. httpPassed: true,
  110. latencyPassed: true,
  111. contentPassed: true,
  112. },
  113. });
  114. const { testProviderUnified } = await import("@/actions/providers");
  115. const result = await testProviderUnified({
  116. providerUrl: "https://api.example.com",
  117. apiKey: "sk-test",
  118. providerType: "openai-compatible",
  119. model: "gpt-4.1-mini",
  120. });
  121. expect(result.ok).toBe(true);
  122. expect(result.data?.success).toBe(true);
  123. expect((result.data as { rawResponse?: string } | undefined)?.rawResponse).toBe(
  124. '{"message":"pong"}'
  125. );
  126. });
  127. test("testProviderGemini 成功时也应该返回完整响应体,保证前端能展示原始 body", async () => {
  128. const responseBody = JSON.stringify({
  129. candidates: [
  130. {
  131. content: {
  132. parts: [{ text: "pong" }],
  133. },
  134. },
  135. ],
  136. usageMetadata: {
  137. promptTokenCount: 2,
  138. candidatesTokenCount: 1,
  139. totalTokenCount: 3,
  140. },
  141. });
  142. fetchMock.mockResolvedValue({
  143. ok: true,
  144. status: 200,
  145. statusText: "OK",
  146. headers: new Headers({
  147. "content-type": "application/json",
  148. }),
  149. text: async () => responseBody,
  150. } as Response);
  151. const { testProviderGemini } = await import("@/actions/providers");
  152. const result = await testProviderGemini({
  153. providerUrl: "https://gemini.example.com",
  154. apiKey: "AIza1234567890abcdefghijklmnopqrstuvwxyz",
  155. model: "gemini-2.5-pro",
  156. });
  157. expect(result.ok).toBe(true);
  158. expect(result.data?.success).toBe(true);
  159. const details = result.data && "details" in result.data ? result.data.details : undefined;
  160. expect((details as { rawResponse?: string } | undefined)?.rawResponse).toBe(responseBody);
  161. });
  162. });