diff --git a/sdks/nodejs-client/src/http/client.test.js b/sdks/nodejs-client/src/http/client.test.js index ae1340b99a..deb1bbcda6 100644 --- a/sdks/nodejs-client/src/http/client.test.js +++ b/sdks/nodejs-client/src/http/client.test.js @@ -39,6 +39,7 @@ const createMockResponse = (options = {}) => { describe("HttpClient", () => { beforeEach(() => { vi.restoreAllMocks(); + vi.useRealTimers(); // Ensure real timers are used by default }); it("builds requests with auth headers and JSON content type", async () => { @@ -66,12 +67,12 @@ describe("HttpClient", () => { }); it("serializes array query params", async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - text: async () => "ok", - }); + const mockFetch = vi.fn().mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); global.fetch = mockFetch; const client = new HttpClient({ apiKey: "test" }); @@ -153,12 +154,12 @@ describe("HttpClient", () => { }); it("respects form-data headers", async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - text: async () => "ok", - }); + const mockFetch = vi.fn().mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); global.fetch = mockFetch; const client = new HttpClient({ apiKey: "test" }); @@ -183,24 +184,25 @@ describe("HttpClient", () => { it("maps 401 and 429 errors", async () => { const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 401, - headers: new Headers(), - text: vi.fn().mockResolvedValue('{"message":"unauthorized"}'), - json: vi.fn().mockResolvedValue({ message: "unauthorized" }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + ok: false, + status: 401, + data: { message: "unauthorized" }, + }) + ); await expect( client.requestRaw({ method: "GET", path: "/meta" }) ).rejects.toBeInstanceOf(AuthenticationError); - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 429, - headers: new Headers({ "retry-after": "2" }), - text: vi.fn().mockResolvedValue('{"message":"rate"}'), - json: vi.fn().mockResolvedValue({ message: "rate" }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + ok: false, + status: 429, + headers: { "retry-after": "2" }, + data: { message: "rate" }, + }) + ); const error = await client .requestRaw({ method: "GET", path: "/meta" }) .catch((err) => err); @@ -211,44 +213,39 @@ describe("HttpClient", () => { it("maps validation and upload errors", async () => { const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 422, - headers: new Headers(), - text: vi.fn().mockResolvedValue('{"message":"invalid"}'), - json: vi.fn().mockResolvedValue({ message: "invalid" }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + ok: false, + status: 422, + data: { message: "invalid" }, + }) + ); await expect( client.requestRaw({ method: "POST", path: "/chat-messages", data: { user: "u" } }) ).rejects.toBeInstanceOf(ValidationError); - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 400, - headers: new Headers(), - text: vi.fn().mockResolvedValue('{"message":"bad upload"}'), - json: vi.fn().mockResolvedValue({ message: "bad upload" }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + ok: false, + status: 400, + data: { message: "bad upload" }, + }) + ); await expect( client.requestRaw({ method: "POST", path: "/files/upload", data: { user: "u" } }) ).rejects.toBeInstanceOf(FileUploadError); }); it("maps timeout and network errors", async () => { - const client = new HttpClient({ apiKey: "test", maxRetries: 0, timeout: 0.001 }); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); - global.fetch = vi.fn().mockImplementation(() => - new Promise((resolve) => setTimeout(() => resolve({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => ({}), - }), 100)) - ); + // Test AbortError (which is what timeout produces) + global.fetch = vi.fn().mockRejectedValue(new DOMException("aborted", "AbortError")); await expect( client.requestRaw({ method: "GET", path: "/meta" }) ).rejects.toBeInstanceOf(TimeoutError); + // Test network error global.fetch = vi.fn().mockRejectedValue(new Error("network")); await expect( client.requestRaw({ method: "GET", path: "/meta" }) @@ -258,12 +255,12 @@ describe("HttpClient", () => { it("retries on timeout errors", async () => { const mockFetch = vi.fn() .mockRejectedValueOnce(new DOMException("aborted", "AbortError")) - .mockResolvedValueOnce({ - ok: true, - status: 200, - headers: new Headers(), - text: async () => "ok", - }); + .mockResolvedValueOnce( + createMockResponse({ + status: 200, + data: "ok", + }) + ); global.fetch = mockFetch; const client = new HttpClient({ apiKey: "test", maxRetries: 1, retryDelay: 0 }); @@ -283,13 +280,13 @@ describe("HttpClient", () => { }); it("returns APIError for other http failures", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: false, - status: 500, - headers: new Headers(), - text: vi.fn().mockResolvedValue('{"message":"server"}'), - json: vi.fn().mockResolvedValue({ message: "server" }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + ok: false, + status: 500, + data: { message: "server" }, + }) + ); const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); await expect( @@ -298,12 +295,12 @@ describe("HttpClient", () => { }); it("logs requests and responses when enableLogging is true", async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => ({ ok: true }), - }); + global.fetch = vi.fn().mockResolvedValue( + createMockResponse({ + status: 200, + data: { ok: true }, + }) + ); const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); const client = new HttpClient({ apiKey: "test", enableLogging: true }); @@ -318,12 +315,12 @@ describe("HttpClient", () => { it("logs retry attempts when enableLogging is true", async () => { const mockFetch = vi.fn() .mockRejectedValueOnce(new DOMException("aborted", "AbortError")) - .mockResolvedValueOnce({ - ok: true, - status: 200, - headers: new Headers(), - text: async () => "ok", - }); + .mockResolvedValueOnce( + createMockResponse({ + status: 200, + data: "ok", + }) + ); global.fetch = mockFetch; const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); diff --git a/sdks/nodejs-client/src/index.test.js b/sdks/nodejs-client/src/index.test.js index c13573aa23..e84a02527a 100644 --- a/sdks/nodejs-client/src/index.test.js +++ b/sdks/nodejs-client/src/index.test.js @@ -3,6 +3,31 @@ import { ChatClient, DifyClient, WorkflowClient, BASE_URL, routes } from "./inde const mockFetch = vi.fn(); +// Helper to create a mock fetch response +const createMockResponse = (options = {}) => { + const { + ok = true, + status = 200, + headers = {}, + body = null, + data = null, + } = options; + + const headersObj = new Headers(headers); + const response = { + ok, + status, + headers: headersObj, + body, + json: vi.fn().mockResolvedValue(data), + text: vi.fn().mockResolvedValue(typeof data === 'string' ? data : JSON.stringify(data)), + blob: vi.fn().mockResolvedValue(new Blob()), + arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)), + }; + + return response; +}; + beforeEach(() => { vi.restoreAllMocks(); mockFetch.mockReset(); @@ -29,12 +54,12 @@ describe("Send Requests", () => { const difyClient = new DifyClient("test"); const method = "GET"; const endpoint = routes.application.url(); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "response", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "response", + }) + ); await difyClient.sendRequest(method, endpoint); @@ -46,12 +71,12 @@ describe("Send Requests", () => { it("uses the getMeta route configuration", async () => { const difyClient = new DifyClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await difyClient.getMeta("end-user"); @@ -85,12 +110,12 @@ describe("File uploads", () => { it("does not override multipart boundary headers for FormData", async () => { const difyClient = new DifyClient("test"); const form = new globalThis.FormData(); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await difyClient.fileUpload(form, "end-user"); @@ -106,12 +131,12 @@ describe("File uploads", () => { describe("Workflow client", () => { it("uses tasks stop path for workflow stop", async () => { const workflowClient = new WorkflowClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "stopped", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "stopped", + }) + ); await workflowClient.stop("task-1", "end-user"); @@ -125,12 +150,12 @@ describe("Workflow client", () => { it("maps workflow log filters to service api params", async () => { const workflowClient = new WorkflowClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await workflowClient.getLogs({ createdAtAfter: "2024-01-01T00:00:00Z", @@ -157,12 +182,12 @@ describe("Workflow client", () => { describe("Chat client", () => { it("places user in query for suggested messages", async () => { const chatClient = new ChatClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await chatClient.getSuggested("msg-1", "end-user"); @@ -175,12 +200,12 @@ describe("Chat client", () => { it("uses last_id when listing conversations", async () => { const chatClient = new ChatClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await chatClient.getConversations("end-user", "last-1", 10); @@ -195,12 +220,12 @@ describe("Chat client", () => { it("lists app feedbacks without user params", async () => { const chatClient = new ChatClient("test"); - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - headers: new Headers(), - json: async () => "ok", - }); + mockFetch.mockResolvedValue( + createMockResponse({ + status: 200, + data: "ok", + }) + ); await chatClient.getAppFeedbacks(1, 20);