@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
640 lines (562 loc) • 20.6 kB
text/typescript
import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer, sleep } from "@hyper-fetch/testing";
import { createAdapter, createDispatcher } from "../../utils";
import type {
HttpAdapterType,
RequestInstance,
RequestResponseEventType,
ResponseDetailsType,
ResponseType,
} from "../../../src";
import { Client, getErrorMessage, mocker } from "../../../src";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("Mocker [ Base ]", () => {
const adapterSpy = vi.fn();
const fixture = { test: 1, data: [200, 300, 404] };
let adapter = createAdapter({ callback: adapterSpy });
let client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
let dispatcher = createDispatcher(client);
let request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" }).setMockingEnabled(true);
beforeAll(() => {
startServer();
});
beforeEach(() => {
resetMocks();
adapter = createAdapter({ callback: adapterSpy });
client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
dispatcher = createDispatcher(client);
request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" }).setMockingEnabled(true);
vi.resetAllMocks();
});
afterAll(() => {
stopServer();
});
describe("When using exec method", () => {
it("should return adapter response", async () => {
const mockedRequest = request.setMock(() => ({ data: fixture, status: 200 }));
const response = await mockedRequest.exec();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
responseTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
});
});
});
describe("When using send method", () => {
it("should return adapter response", async () => {
const mockedRequest = request.setMock(() => ({ data: fixture, status: 200 }));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
responseTimestamp: expect.toBeNumber(),
requestTimestamp: expect.toBeNumber(),
});
});
});
it("should return timeout error when request takes too long", async () => {
const mockedRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(
() => ({
data: fixture,
status: 200,
}),
{ responseTime: 150, timeout: 20 },
);
const response = await mockedRequest.send();
expect(response.data).toBe(null);
expect(response.error?.message).toEqual(getErrorMessage("timeout").message);
});
it("should allow to cancel single running request", async () => {
const firstSpy = vi.fn();
const secondSpy = vi.fn();
const firstRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(
() => ({
data: fixture,
status: 200,
}),
{
responseTime: 150,
},
);
const secondRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(
() => ({
data: fixture,
status: 200,
}),
{
responseTime: 150,
},
);
dispatcher.add(secondRequest);
const requestId = dispatcher.add(firstRequest);
client.requestManager.events.onAbortById(requestId, firstSpy);
client.requestManager.events.onAbortByKey(firstRequest.abortKey, secondSpy);
await sleep(5);
dispatcher.cancelRunningRequest(firstRequest.queryKey, requestId);
expect(dispatcher.getRunningRequests(firstRequest.queryKey)).toHaveLength(1);
expect(firstSpy).toHaveBeenCalledTimes(1);
expect(secondSpy).toHaveBeenCalledTimes(1);
});
it("should allow for retrying request", async () => {
let response: RequestResponseEventType<RequestInstance>;
let index = 0;
const fixtures = [
{ data: { data: [1, 2, 3] }, status: 400, success: false },
{ data: { data: [1, 2, 3] }, status: 200 },
];
const requestWithRetry = request
.setRetry(1)
.setRetryTime(50)
.setMock(() => {
if (index === 1) {
index = 0;
return fixtures[1];
}
index = 1;
return fixtures[0];
});
client.requestManager.events.onResponseByCache(requestWithRetry.cacheKey, (event) => {
if (event.details.retries) {
response = event;
}
});
dispatcher.add(requestWithRetry);
await waitFor(() => {
expect(response).toBeDefined();
});
const adapterResponse: ResponseType<unknown, unknown, HttpAdapterType> = {
data: { data: [1, 2, 3] },
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
};
const responseDetails: Omit<ResponseDetailsType, "timestamp"> = {
retries: 1,
isCanceled: false,
isOffline: false,
willRetry: false,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
addedTimestamp: expect.toBeNumber(),
triggerTimestamp: expect.toBeNumber(),
};
await waitFor(() => {
expect(response).toStrictEqual({
requestId: expect.toBeString(),
request: requestWithRetry,
response: adapterResponse,
details: responseDetails,
});
});
});
it("should cycle through sequence if provided array of responses", async () => {
let index = 0;
const fixtures = [
{ data: { data: [1, 2, 3] }, status: 200 },
{ data: { data: [4, 5, 6] }, status: 200 },
];
const requestWithRetry = request.setMock(() => {
if (index === 1) {
index = 0;
return fixtures[1];
}
index = 1;
return fixtures[0];
});
const response1 = await requestWithRetry.send();
const response2 = await requestWithRetry.send();
const response3 = await requestWithRetry.send();
const response4 = await requestWithRetry.send();
expect(response1.data).toStrictEqual({ data: [1, 2, 3] });
expect(response2.data).toStrictEqual({ data: [4, 5, 6] });
expect(response2.status).toStrictEqual(200);
expect(response3.data).toStrictEqual({ data: [1, 2, 3] });
expect(response4.data).toStrictEqual({ data: [4, 5, 6] });
});
it("should allow for passing method to mock and return data conditionally", async () => {
const mockedRequest = client.createRequest<{ response: any }>()({ endpoint: "/users/:id" });
mockedRequest.setMock(({ request: req }) => {
const { params } = req;
if (params?.id === 11) {
return { data: [1, 2, 3], status: 222 };
}
return { data: [4, 5, 6], status: 200 };
});
const response = await mockedRequest.send({ params: { id: 11 } } as any);
const response2 = await mockedRequest.send({ params: { id: 13 } } as any);
expect(response.data).toStrictEqual([1, 2, 3]);
expect(response2.data).toStrictEqual([4, 5, 6]);
expect(response.status).toStrictEqual(222);
expect(response2.status).toStrictEqual(200);
});
it("should allow for passing async method to mock and return data conditionally", async () => {
const mockedRequest = client
.createRequest<{ response: Record<string, any> }>()({ endpoint: "users/:id" })
.setMock(async ({ request: r }) => {
if (r?.params?.id === 1) {
return { data: [1, 2, 3], status: 222 };
}
return { data: [4, 5, 6], status: 200 };
});
const response = await mockedRequest.send({ params: { id: 1 } } as any);
const response2 = await mockedRequest.send({ params: { id: 2 } } as any);
expect(response.data).toStrictEqual([1, 2, 3]);
expect(response2.data).toStrictEqual([4, 5, 6]);
expect(response.status).toStrictEqual(222);
expect(response2.status).toStrictEqual(200);
});
it("should allow for passing multiple functions and cycle through them", async () => {
let index = 0;
const firstFunction = (r: typeof mockedRequest) => {
if (r.params?.id === 1) {
return { data: [1, 2, 3], status: 200 };
}
return { data: [4, 5, 6], status: 200 };
};
const secondFunction = (r: typeof mockedRequest) => {
if (r.params?.id === 1) {
return { data: [42, 42, 42], status: 200 };
}
return { data: [19, 19, 19], status: 200 };
};
const mockedRequest = client
.createRequest<{ response: any }>()({ endpoint: "users/:id" })
.setMock(({ request: r }) => {
if (index === 1) {
index = 0;
return secondFunction(r);
}
index = 1;
return firstFunction(r);
});
const response1 = await mockedRequest.send({ params: { id: 1 } } as any);
const response2 = await mockedRequest.send({ params: { id: 1 } } as any);
const response3 = await mockedRequest.send({ params: { id: 1 } } as any);
const response4 = await mockedRequest.send({ params: { id: 11 } } as any);
const response5 = await mockedRequest.send({ params: { id: 12 } } as any);
expect(response1.data).toStrictEqual([1, 2, 3]);
expect(response2.data).toStrictEqual([42, 42, 42]);
expect(response3.data).toStrictEqual([1, 2, 3]);
expect(response4.data).toStrictEqual([19, 19, 19]);
expect(response5.data).toStrictEqual([4, 5, 6]);
});
it("should allow for removing mocker and expecting normal behavior when executing request", async () => {
const mockedRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(() => ({ data: fixture, status: 200 }));
const response = await mockedRequest.send();
mockedRequest.clearMock();
const data = mockRequest(mockedRequest, { data: { data: [64, 64, 64] } });
const response2 = await mockedRequest.send();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
expect(response2.data).toStrictEqual(data);
});
it("should allow for mocking extra", async () => {
const mockedRequest = request.setMock(() => ({
data: fixture,
extra: { headers: { something: "true" } },
status: 213,
}));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 213,
success: true,
extra: { headers: { something: "true" } },
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should allow for setting status that is not a number", async () => {
const mockedRequest = request.setMock(() => ({
data: fixture,
extra: { someExtra: true } as any,
status: "success" as any,
}));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: "success",
success: true,
extra: { someExtra: true },
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should adjust responseTime and requestTime", async () => {
const mockedRequest = request.setMock(
() => ({
data: fixture,
status: 200,
}),
{ requestTime: 100, responseTime: 100 },
);
const startDate = +new Date();
const response = await mockedRequest.send();
const endDate = +new Date();
expect(endDate - startDate).toBeGreaterThanOrEqual(200);
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should allow for setting totalUploaded and totalDownloaded", async () => {
const mockedRequest = request.setMock(
() => ({
data: fixture,
status: 200,
}),
{ totalUploaded: 1000, requestTime: 40, totalDownloaded: 1000, responseTime: 60 },
);
const requestSpy = vi.fn();
const responseSpy = vi.fn();
client.requestManager.events.onUploadProgressByQueue(request.queryKey, requestSpy);
client.requestManager.events.onDownloadProgressByQueue(request.queryKey, responseSpy);
const response = await mockedRequest.send();
expect(requestSpy.mock.calls.length).toBeGreaterThanOrEqual(3);
expect(responseSpy.mock.calls.length).toBeGreaterThanOrEqual(3);
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should allow for toggling the mocker off and then turning it on again without removal", async () => {
const mockedRequest = request.setMock(() => ({ data: fixture, status: 200 }));
const data = mockRequest(mockedRequest, { data: { data: [42, 42, 42] } });
expect(mockedRequest.isMockerEnabled).toBe(true);
mockedRequest.setMockingEnabled(false);
expect(mockedRequest.isMockerEnabled).toBe(false);
const notMockedData = await mockedRequest.send();
mockedRequest.setMockingEnabled(true);
const mockedData = await mockedRequest.send();
expect(notMockedData).toStrictEqual({
data,
error: null,
status: 200,
success: true,
extra: {
headers: {
"content-length": "19",
"content-type": "application/json",
},
},
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
expect(mockedData).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("Should allow for globally turning on and off for all requests related to a client", async () => {
const newClient = new Client({ url: "shared-base-url" }).setAdapter(adapter);
const mockedRequest = newClient
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(() => ({ data: fixture, status: 200 }));
const data = mockRequest(mockedRequest, { data: { data: [42, 42, 42] } });
expect(mockedRequest.isMockerEnabled).toBe(true);
expect(newClient.isMockerEnabled).toBe(true);
const mockedDataDefault = await mockedRequest.send();
newClient.setEnableGlobalMocking(false);
const notMockedData = await mockedRequest.send();
newClient.setEnableGlobalMocking(true);
const mockedDataAfter = await mockedRequest.send();
expect(mockedDataDefault).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
expect(mockedDataAfter).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
expect(notMockedData).toStrictEqual({
data,
error: null,
status: 200,
success: true,
extra: {
headers: {
"content-type": "application/json",
"content-length": "19",
},
},
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should throw error when mock is not defined", async () => {
await expect(
mocker({
request: client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" }),
} as any),
).rejects.toThrow("Request processing error");
});
describe("When mock returns error without success and without data", () => {
it("should call onError path when success is false and no data", async () => {
const errorObj = new Error("mock error");
const mockedRequest = request.setMock(() => ({
data: null,
error: errorObj,
status: 500,
success: false,
}));
const response = await mockedRequest.send();
expect(response.data).toBe(null);
expect(response.error).toEqual(errorObj);
expect(response.status).toBe(500);
expect(response.success).toBe(false);
});
});
describe("When mock config has no timeout", () => {
it("should skip timeout setup when timeout is not a number", async () => {
const mockedRequest = request.setMock(
() => ({
data: fixture,
status: 200,
}),
{ requestTime: 20, responseTime: 20 },
);
const response = await mockedRequest.send();
expect(response.data).toStrictEqual(fixture);
expect(response.status).toBe(200);
expect(response.success).toBe(true);
});
});
describe("When request is aborted before progress completes", () => {
it("should handle abort via timeout=0 before response progress starts", async () => {
const mockedRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(
() => ({
data: fixture,
status: 200,
}),
{ requestTime: 100, responseTime: 100, timeout: 0 },
);
const response = await mockedRequest.send();
expect(response.data).toBe(null);
expect(response.error).toBeDefined();
});
it("should resolve progress immediately when abort already triggered", async () => {
const mockedRequest = client
.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
.setMock(
() => ({
data: fixture,
status: 200,
}),
{ requestTime: 200, responseTime: 200, timeout: 1 },
);
const response = await mockedRequest.send();
expect(response.data).toBe(null);
expect(response.error).toBeDefined();
});
});
describe("When handling different response scenarios", () => {
it("should handle success with data", async () => {
const mockedRequest = request.setMock(() => ({
data: fixture,
status: 200,
success: true,
}));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data: fixture,
error: null,
status: 200,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should handle error with data", async () => {
const data = { message: "Error occurred", code: 400 };
const mockedRequest = request.setMock(() => ({
data,
error: new Error("Test error"),
status: 400,
success: true,
}));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data,
error: expect.any(Error),
status: 400,
success: true,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
it("should handle error without data", async () => {
const mockedRequest = request.setMock(() => ({
data: null,
error: new Error("Test error"),
status: 500,
success: false,
}));
const response = await mockedRequest.send();
expect(response).toStrictEqual({
data: null,
error: expect.any(Error),
status: 500,
success: false,
extra: request.client.adapter.defaultExtra,
requestTimestamp: expect.toBeNumber(),
responseTimestamp: expect.toBeNumber(),
});
});
});
});