@hyper-fetch/core
Version:
Cache, Queue and Persist your requests no matter if you are online or offline!
360 lines (286 loc) • 11.2 kB
text/typescript
import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer } from "@hyper-fetch/testing";
import { Client } from "client";
const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();
describe("Request [ retryOnError ]", () => {
let client = new Client({ url: "shared-base-url" });
beforeAll(() => {
startServer();
});
beforeEach(() => {
client = new Client({ url: "shared-base-url" });
resetMocks();
vi.resetAllMocks();
});
afterAll(() => {
stopServer();
});
describe("When setting retryOnError via setRetryOnError", () => {
it("should set the retryOnError callback", () => {
const request = client.createRequest()({ endpoint: "/users", retry: 2 });
const callback = () => true;
expect(request.retryOnError).toBeUndefined();
const updated = request.setRetryOnError(callback);
expect(updated.retryOnError).toBeDefined();
});
it("should inherit retryOnError through clone", () => {
const request = client.createRequest()({ endpoint: "/users", retry: 2 });
const callback = () => true;
const updated = request.setRetryOnError(callback);
const cloned = updated.setHeaders({ "x-custom": "test" });
expect(cloned.retryOnError).toBeDefined();
});
it("should not affect original when clone retryOnError is modified", () => {
const request = client.createRequest()({ endpoint: "/users", retry: 2 });
const callback = () => true;
const updated = request.setRetryOnError(callback);
const cloned = updated.setHeaders({ "x-custom": "test" });
cloned.retryOnError = () => false;
expect(updated.retryOnError).toBeDefined();
expect(updated.retryOnError!(null as any)).toBe(true);
});
it("should return a new request instance (immutable)", () => {
const request = client.createRequest()({ endpoint: "/users", retry: 2 });
const updated = request.setRetryOnError(() => true);
expect(updated).not.toBe(request);
expect(request.retryOnError).toBeUndefined();
});
});
describe("When retryOnError returns true (allow retry)", () => {
it("should retry the request as normal", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 2,
retryTime: 0,
});
mockRequest(request, { status: 400, delay: 0 });
const retryOnErrorSpy = vi.fn().mockReturnValue(true);
const retryingRequest = request.setRetryOnError(retryOnErrorSpy);
const spy = vi.spyOn(client.adapter, "fetch");
retryingRequest.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(3);
});
// Called once per attempt where retry is still available (not on the last exhausted attempt)
expect(retryOnErrorSpy).toHaveBeenCalledTimes(2);
});
it("should call retryOnError with the response object", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 1,
retryTime: 0,
});
mockRequest(request, { status: 500, delay: 0 });
const retryOnErrorSpy = vi.fn().mockReturnValue(true);
const retryingRequest = request.setRetryOnError(retryOnErrorSpy);
retryingRequest.send();
await waitFor(() => {
expect(retryOnErrorSpy).toHaveBeenCalledTimes(1);
});
const firstCallArg = retryOnErrorSpy.mock.calls[0][0];
expect(firstCallArg).toHaveProperty("error");
expect(firstCallArg).toHaveProperty("status");
expect(firstCallArg).toHaveProperty("success", false);
expect(firstCallArg).toHaveProperty("data");
expect(firstCallArg.status).toBe(500);
});
});
describe("When retryOnError returns false (stop retry)", () => {
it("should not retry when retryOnError returns false", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 3,
retryTime: 0,
});
mockRequest(request, { status: 404, delay: 0 });
const retryingRequest = request.setRetryOnError(() => false);
const spy = vi.spyOn(client.adapter, "fetch");
retryingRequest.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
await new Promise((r) => {
setTimeout(r, 50);
});
expect(spy).toHaveBeenCalledTimes(1);
});
it("should resolve the send promise immediately when retryOnError returns false", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 3,
retryTime: 0,
});
mockRequest(request, { status: 404, delay: 0 });
const retryingRequest = request.setRetryOnError(() => false);
const { status, success } = await retryingRequest.send();
expect(status).toBe(404);
expect(success).toBe(false);
});
it("should stop retrying based on status code (e.g. 404)", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 5,
retryTime: 0,
});
mockRequest(request, { status: 404, delay: 0 });
const retryingRequest = request.setRetryOnError((response) => {
return response.status !== 404;
});
const spy = vi.spyOn(client.adapter, "fetch");
const result = await retryingRequest.send();
expect(spy).toHaveBeenCalledTimes(1);
expect(result.status).toBe(404);
expect(result.success).toBe(false);
});
it("should stop retrying based on error content", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 5,
retryTime: 0,
});
mockRequest(request, { status: 403, delay: 0 });
const retryingRequest = request.setRetryOnError((response) => {
return response.status !== 403;
});
const spy = vi.spyOn(client.adapter, "fetch");
const result = await retryingRequest.send();
expect(spy).toHaveBeenCalledTimes(1);
expect(result.status).toBe(403);
});
});
describe("When retryOnError conditionally allows or disallows retries", () => {
it("should retry on 500 but not on 404", async () => {
let callCount = 0;
const request = client.createRequest()({
endpoint: "/users",
retry: 5,
retryTime: 0,
});
const retryOnErrorSpy = vi.fn().mockImplementation((response) => {
return response.status !== 404;
});
const retryingRequest = request.setRetryOnError(retryOnErrorSpy);
const originalFetch = client.adapter.fetch.bind(client.adapter);
vi.spyOn(client.adapter, "fetch").mockImplementation(async (...args) => {
callCount += 1;
if (callCount === 1) {
mockRequest(request, { status: 500, delay: 0 });
} else {
mockRequest(request, { status: 404, delay: 0 });
}
return originalFetch(...args);
});
mockRequest(request, { status: 500, delay: 0 });
const result = await retryingRequest.send();
expect(result.status).toBe(404);
expect(result.success).toBe(false);
});
});
describe("When retryOnError is not set", () => {
it("should retry normally without retryOnError", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 2,
retryTime: 0,
});
mockRequest(request, { status: 400, delay: 0 });
const spy = vi.spyOn(client.adapter, "fetch");
request.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(3);
});
});
it("should not retry when retry is 0 and no retryOnError", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 0,
});
mockRequest(request, { status: 400, delay: 0 });
const spy = vi.spyOn(client.adapter, "fetch");
request.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(1);
});
});
});
describe("When retryOnError interacts with successful responses", () => {
it("should not call retryOnError on successful response", async () => {
const request = client.createRequest<{ response: { name: string } }>()({
endpoint: "/users",
retry: 3,
retryTime: 0,
});
mockRequest(request, { status: 200, delay: 0, data: { name: "John" } });
const retryOnErrorSpy = vi.fn().mockReturnValue(true);
const retryingRequest = request.setRetryOnError(retryOnErrorSpy);
const result = await retryingRequest.send();
expect(result.success).toBe(true);
expect(retryOnErrorSpy).not.toHaveBeenCalled();
});
});
describe("When retryOnError is used with send() callbacks", () => {
it("should fire onResponse when retryOnError stops retries", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 3,
retryTime: 0,
});
mockRequest(request, { status: 404, delay: 0 });
const onResponseSpy = vi.fn();
const retryingRequest = request.setRetryOnError((response) => response.status !== 404);
await retryingRequest.send({ onResponse: onResponseSpy });
expect(onResponseSpy).toHaveBeenCalledTimes(1);
});
});
describe("When retryOnError is used with $hooks", () => {
it("should fire $hooks.onResponse when retryOnError stops retries", async () => {
const request = client.createRequest()({
endpoint: "/users",
retry: 3,
retryTime: 0,
});
mockRequest(request, { status: 404, delay: 0 });
const hookSpy = vi.fn();
const retryingRequest = request.setRetryOnError((response) => response.status !== 404);
retryingRequest.$hooks.onResponse(hookSpy);
await retryingRequest.send();
expect(hookSpy).toHaveBeenCalledTimes(1);
});
});
describe("When using setRetry chained with setRetryOnError", () => {
it("should respect both retry count and retryOnError", async () => {
const request = client
.createRequest()({ endpoint: "/users" })
.setRetry(3)
.setRetryTime(0)
.setRetryOnError(() => true);
mockRequest(request, { status: 400, delay: 0 });
const spy = vi.spyOn(client.adapter, "fetch");
request.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(4);
});
});
it("should stop early when retryOnError returns false mid-retry", async () => {
let attempt = 0;
const request = client
.createRequest()({ endpoint: "/users" })
.setRetry(5)
.setRetryTime(0)
.setRetryOnError(() => {
attempt += 1;
return attempt <= 2;
});
mockRequest(request, { status: 500, delay: 0 });
const spy = vi.spyOn(client.adapter, "fetch");
request.send();
await waitFor(() => {
expect(spy).toHaveBeenCalledTimes(3);
});
await new Promise((r) => {
setTimeout(r, 50);
});
expect(spy).toHaveBeenCalledTimes(3);
});
});
});