@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
370 lines (294 loc) • 12.2 kB
text/typescript
/**
* @vitest-environment node
*/
import type { Mock } from "vitest";
import { createHttpMockingServer } from "@hyper-fetch/testing";
import { getErrorMessage } from "adapter";
import { Client } from "client";
import { HttpAdapter } from "http-adapter";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("Http Adapter [ Fetch ]", () => {
const requestId = "test";
const abortKey = "abort-key";
let client = new Client({ url: "http://shared-base-url" });
let request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint", abortKey });
beforeAll(() => {
startServer();
});
beforeEach(() => {
client = new Client({ url: "http://shared-base-url" });
client.appManager.isBrowser = false;
request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint", abortKey });
client.requestManager.addAbortController(abortKey, requestId);
resetMocks();
vi.resetAllMocks();
vi.clearAllMocks();
vi.restoreAllMocks();
});
afterAll(() => {
stopServer();
});
it("should pick correct adapter and not throw", async () => {
mockRequest(request);
client.appManager.isBrowser = true;
await expect(() => HttpAdapter().initialize(client).fetch(request, requestId)).not.toThrow();
});
it("should make a request and return success data with status", async () => {
const data = mockRequest(request, { data: { response: [] } });
const { data: response, error, status, extra } = await HttpAdapter().initialize(client).fetch(request, requestId);
expect(response).toStrictEqual(data);
expect(status).toBe(200);
expect(error).toBe(null);
expect(extra).toEqual({ headers: { "content-type": "application/json", "content-length": "15" } });
});
it("should make a request and return error data with status", async () => {
const data = mockRequest(request, { status: 400 });
const { data: response, error, status, extra } = await HttpAdapter().initialize(client).fetch(request, requestId);
expect(response).toBe(null);
expect(status).toBe(400);
expect(error).toStrictEqual(data);
expect(extra).toEqual({ headers: { "content-type": "application/json", "content-length": "19" } });
});
it("should allow to cancel request and return error", async () => {
mockRequest(request, { delay: 5 });
const id = setTimeout(() => {
request.abort();
}, 2);
const { data: response, error } = await HttpAdapter().initialize(client).fetch(request, requestId);
expect(response).toBe(null);
expect(error.message).toEqual(getErrorMessage("abort").message);
clearTimeout(id);
});
it("should return timeout error when request takes too long", async () => {
const timeoutRequest = request.setOptions({ timeout: 10 });
mockRequest(timeoutRequest, { delay: 20 });
const { data: response, error } = await HttpAdapter().initialize(client).fetch(timeoutRequest, requestId);
expect(response).toBe(null);
expect(error.message).toEqual(getErrorMessage("timeout").message);
});
it("should allow to make post request with json data", async () => {
const payload = {
testData: "123",
};
const postRequest = client
.createRequest<{
response: null;
payload: { testData: string };
}>()({ endpoint: "shared-endpoint", method: "POST" })
.setPayload(payload);
client.requestManager.addAbortController(postRequest.abortKey, requestId);
const mock = mockRequest(postRequest);
const {
data: response,
error,
status,
extra,
} = await HttpAdapter().initialize(client).fetch(postRequest, requestId);
expect(response).toEqual(mock);
expect(error).toBeNull();
expect(status).toEqual(200);
expect(extra).toEqual({ headers: { "content-type": "application/json", "content-length": "2" } });
});
it("should send payload with object data", async () => {
const mutation = client.createRequest<{ response: any; payload: any }>()({
endpoint: "/shared-endpoint",
method: "POST",
});
client.requestManager.addAbortController(mutation.abortKey, requestId);
mockRequest(mutation);
const { error, status } = await mutation.send({
payload: {
username: "Kacper",
password: "Kacper1234",
},
});
expect(error).toBeNull();
expect(status).toBe(200);
});
it("should send payload with string data", async () => {
const mutation = client.createRequest<{ response: any; payload: any }>()({
endpoint: "/shared-endpoint",
method: "POST",
});
client.requestManager.addAbortController(mutation.abortKey, requestId);
mockRequest(mutation);
const { error, status } = await mutation.send({
payload: "raw-string-payload",
});
expect(error).toBeNull();
expect(status).toBe(200);
});
describe("When streaming mode is enabled", () => {
it("should return response.body for successful streaming response", async () => {
mockRequest(request, { data: { response: "stream" } });
const streamRequest = request.setOptions({ streaming: true });
const result = await HttpAdapter().initialize(client).fetch(streamRequest, requestId);
expect(result.status).toBe(200);
expect(result.data).toBeDefined();
});
it("should return error for failed streaming response", async () => {
mockRequest(request, { status: 400, data: { error: "stream-fail" } });
const streamRequest = request.setOptions({ streaming: true });
const result = await HttpAdapter().initialize(client).fetch(streamRequest, requestId);
expect(result.status).toBe(400);
expect(result.data).toBe(null);
expect(result.error).toBeDefined();
});
});
describe("When response.body has no getReader", () => {
it("should fall back to response.text()", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/json" }),
body: null,
text: vi.fn().mockResolvedValue(JSON.stringify({ fallback: true })),
});
try {
const adapter = HttpAdapter().initialize(client);
const result = await adapter.fetch(request, requestId);
expect(result.status).toBe(200);
expect(result.data).toStrictEqual({ fallback: true });
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe("When non-timeout, non-abort error occurs", () => {
it("should call onError with the error", async () => {
const originalFetch = globalThis.fetch;
const testError = new Error("Network failure");
globalThis.fetch = vi.fn().mockRejectedValue(testError);
try {
const adapter = HttpAdapter().initialize(client);
const result = await adapter.fetch(request, requestId);
expect(result.data).toBe(null);
expect(result.error).toBe(testError);
expect(result.status).toBe(0);
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe("When sending non-string payload with POST method", () => {
it("should handle non-string body without triggering string upload progress", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/octet-stream" }),
body: null,
text: vi.fn().mockResolvedValue("ok"),
});
try {
const mutation = client.createRequest<{ response: any; payload: any }>()({
endpoint: "/shared-endpoint",
method: "POST",
abortKey,
});
const bufPayload = Buffer.from("binary data");
client.adapter.setPayloadMapper(() => bufPayload as any);
const req = mutation.setPayload("ignored");
const result = await client.adapter.fetch(req, requestId);
const fetchCall = (globalThis.fetch as unknown as Mock).mock.calls[0][1];
expect(fetchCall.body).toBe(bufPayload);
expect(result.status).toBe(200);
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe("When response has content-length header in streaming reader path", () => {
it("should use content-length for progress total when available", async () => {
const originalFetch = globalThis.fetch;
const bodyContent = JSON.stringify({ data: "test" });
const encoder = new TextEncoder();
const encoded = encoder.encode(bodyContent);
const mockReader = {
read: vi
.fn()
.mockResolvedValueOnce({ done: false, value: encoded })
.mockResolvedValueOnce({ done: true, value: undefined }),
};
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers({
"content-type": "application/json",
"content-length": String(encoded.length),
}),
body: { getReader: () => mockReader },
});
try {
const adapter = HttpAdapter().initialize(client);
const result = await adapter.fetch(request, requestId);
expect(result.status).toBe(200);
expect(result.data).toStrictEqual({ data: "test" });
} finally {
globalThis.fetch = originalFetch;
}
});
it("should use receivedLength as total when content-length is missing", async () => {
const originalFetch = globalThis.fetch;
const bodyContent = JSON.stringify({ noLength: true });
const encoder = new TextEncoder();
const encoded = encoder.encode(bodyContent);
const mockReader = {
read: vi
.fn()
.mockResolvedValueOnce({ done: false, value: encoded })
.mockResolvedValueOnce({ done: true, value: undefined }),
};
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: new Headers({ "content-type": "application/json" }),
body: { getReader: () => mockReader },
});
try {
const adapter = HttpAdapter().initialize(client);
const result = await adapter.fetch(request, requestId);
expect(result.status).toBe(200);
expect(result.data).toStrictEqual({ noLength: true });
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe("When error occurs without timeout set", () => {
it("should handle catch block when timeoutId is not set", async () => {
const originalFetch = globalThis.fetch;
const testError = new Error("Connection refused");
globalThis.fetch = vi.fn().mockRejectedValue(testError);
try {
const noTimeoutRequest = request.setOptions({ timeout: 0 });
const adapter = HttpAdapter().initialize(client);
const result = await adapter.fetch(noTimeoutRequest, requestId);
expect(result.data).toBe(null);
expect(result.error).toBe(testError);
expect(result.status).toBe(0);
} finally {
globalThis.fetch = originalFetch;
}
});
});
describe("When timeoutMs is 0 or negative", () => {
it("should skip timeout setup when timeout is 0 and still succeed", async () => {
const data = mockRequest(request, { data: { response: [] } });
const zeroTimeoutRequest = request.setOptions({ timeout: 0 });
const result = await zeroTimeoutRequest.send();
expect(result.status).toBe(200);
expect(result.error).toBe(null);
expect(result.data).toStrictEqual(data);
});
it("should skip timeout setup when timeout is negative and still succeed", async () => {
const data = mockRequest(request, { data: { response: [] } });
const negativeTimeoutRequest = request.setOptions({ timeout: -1 });
const result = await negativeTimeoutRequest.send();
expect(result.status).toBe(200);
expect(result.error).toBe(null);
expect(result.data).toStrictEqual(data);
});
});
});