mirror of
https://github.com/langgenius/dify.git
synced 2026-02-18 17:04:41 +08:00
fix(nodejs-client): fix all test mocks for fetch API
- Add createMockResponse helper to properly mock fetch responses - Update all test cases to use the mock helper with all required methods - Fix timeout error test to use AbortError instead of fake timers - Ensure beforeEach restores real timers to avoid test interference - All 85 tests now passing
This commit is contained in:
parent
a4495ab586
commit
df67842fae
@ -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(() => {});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user