import { Readable } from "node:stream"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from "./index"; const stubFetch = (): ReturnType => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); return fetchMock; }; const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => new Response(JSON.stringify(body), { status: 200, ...init, headers: { "content-type": "application/json", ...(init.headers ?? {}), }, }); describe("Client", () => { beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("creates a client with default settings", () => { const difyClient = new DifyClient("test"); expect(difyClient.getHttpClient().getSettings()).toMatchObject({ apiKey: "test", baseUrl: BASE_URL, timeout: 60, }); }); it("updates the api key", () => { const difyClient = new DifyClient("test"); difyClient.updateApiKey("test2"); expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2"); }); }); describe("Send Requests", () => { beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("makes a successful request to the application parameter route", async () => { const fetchMock = stubFetch(); const difyClient = new DifyClient("test"); const method = "GET"; const endpoint = routes.application.url(); fetchMock.mockResolvedValueOnce(jsonResponse("response")); const response = await difyClient.sendRequest(method, endpoint); expect(response).toMatchObject({ status: 200, data: "response", headers: { "content-type": "application/json", }, }); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${endpoint}`); expect(init.method).toBe(method); expect(init.headers).toMatchObject({ Authorization: "Bearer test", "User-Agent": "dify-client-node", }); }); it("uses the getMeta route configuration", async () => { const fetchMock = stubFetch(); const difyClient = new DifyClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); await difyClient.getMeta("end-user"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`); expect(init.method).toBe(routes.getMeta.method); expect(init.headers).toMatchObject({ Authorization: "Bearer test", }); }); }); describe("File uploads", () => { const OriginalFormData = globalThis.FormData; beforeAll(() => { globalThis.FormData = class FormDataMock extends Readable { constructor() { super(); } _read() {} append() {} getHeaders() { return { "content-type": "multipart/form-data; boundary=test", }; } } as unknown as typeof FormData; }); afterAll(() => { globalThis.FormData = OriginalFormData; }); beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("does not override multipart boundary headers for legacy FormData", async () => { const fetchMock = stubFetch(); const difyClient = new DifyClient("test"); const form = new globalThis.FormData(); fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); await difyClient.fileUpload(form, "end-user"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`); expect(init.method).toBe(routes.fileUpload.method); expect(init.headers).toMatchObject({ Authorization: "Bearer test", "content-type": "multipart/form-data; boundary=test", }); expect(init.body).not.toBe(form); expect((init as RequestInit & { duplex?: string }).duplex).toBe("half"); }); }); describe("Workflow client", () => { beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("uses tasks stop path for workflow stop", async () => { const fetchMock = stubFetch(); const workflowClient = new WorkflowClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success" })); await workflowClient.stop("task-1", "end-user"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url("task-1")}`); expect(init.method).toBe(routes.stopWorkflow.method); expect(init.headers).toMatchObject({ Authorization: "Bearer test", "Content-Type": "application/json", }); expect(init.body).toBe(JSON.stringify({ user: "end-user" })); }); it("maps workflow log filters to service api params", async () => { const fetchMock = stubFetch(); const workflowClient = new WorkflowClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); await workflowClient.getLogs({ createdAtAfter: "2024-01-01T00:00:00Z", createdAtBefore: "2024-01-02T00:00:00Z", createdByEndUserSessionId: "sess-1", createdByAccount: "acc-1", page: 2, limit: 10, }); const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; const parsedUrl = new URL(url); expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`); expect(parsedUrl.searchParams.get("created_at__before")).toBe( "2024-01-02T00:00:00Z" ); expect(parsedUrl.searchParams.get("created_at__after")).toBe( "2024-01-01T00:00:00Z" ); expect(parsedUrl.searchParams.get("created_by_end_user_session_id")).toBe( "sess-1" ); expect(parsedUrl.searchParams.get("created_by_account")).toBe("acc-1"); expect(parsedUrl.searchParams.get("page")).toBe("2"); expect(parsedUrl.searchParams.get("limit")).toBe("10"); }); }); describe("Chat client", () => { beforeEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); it("places user in query for suggested messages", async () => { const fetchMock = stubFetch(); const chatClient = new ChatClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success", data: [] })); await chatClient.getSuggested("msg-1", "end-user"); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${routes.getSuggested.url("msg-1")}?user=end-user`); expect(init.method).toBe(routes.getSuggested.method); expect(init.headers).toMatchObject({ Authorization: "Bearer test", }); }); it("uses last_id when listing conversations", async () => { const fetchMock = stubFetch(); const chatClient = new ChatClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true })); await chatClient.getConversations("end-user", "last-1", 10); const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`); }); it("lists app feedbacks without user params", async () => { const fetchMock = stubFetch(); const chatClient = new ChatClient("test"); fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] })); await chatClient.getAppFeedbacks(1, 20); const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`); }); });