import { afterEach, describe, expect, it, vi } from "vitest"; import type { ISbFetch } from "./sbFetch"; import SbFetch from "./sbFetch"; import { headersToObject } from "../tests/utils"; import type { ISbStoriesParams } from "./interfaces"; describe("sbFetch", () => { let sbFetch: SbFetch; const mockFetch = vi.fn(); afterEach(() => { vi.restoreAllMocks(); }); it("should initialize", () => { sbFetch = new SbFetch({} as ISbFetch); expect(sbFetch).toBeInstanceOf(SbFetch); }); describe("get", () => { it("should correctly construct URLs for GET requests", async () => { sbFetch = new SbFetch({ baseURL: "https://api.storyblok.com/v2/", fetch: mockFetch, } as ISbFetch); const response = new Response(JSON.stringify({ data: "test" }), { status: 200, headers: { "Content-Type": "application/json" }, }); mockFetch.mockResolvedValue(response); await sbFetch.get("test", { is_startpage: false, search_term: "test", }); expect(mockFetch).toHaveBeenCalledWith( "https://api.storyblok.com/v2/test?is_startpage=false&search_term=test", expect.anything(), ); }); }); describe("post", () => { it("should handle POST requests correctly", async () => { const testPayload = { title: "New Story" }; const response = new Response(JSON.stringify({ data: "test" }), { status: 200, headers: { "Content-Type": "application/json" }, }); mockFetch.mockResolvedValue(response); await sbFetch.post("stories", testPayload); expect(mockFetch).toHaveBeenCalledWith("https://api.storyblok.com/v2/stories", { method: "post", body: JSON.stringify(testPayload), headers: expect.any(Headers), signal: expect.any(AbortSignal), }); }); it("should set specific headers for POST requests", async () => { sbFetch = new SbFetch({ baseURL: "https://api.storyblok.com/v2/", headers: new Headers({ "Content-Type": "application/json", }), fetch: mockFetch, } as ISbFetch); const testPayload = { title: "New Story" }; const response = new Response(JSON.stringify({ data: "test" }), { status: 200, headers: { "Content-Type": "application/json" }, }); mockFetch.mockResolvedValue(response); await sbFetch.post("stories", testPayload); // Get the last call to fetch and extract the headers const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1]; const actualHeaders = headersToObject(lastCall[1].headers); expect(actualHeaders["content-type"]).toBe("application/json"); }); }); describe("put", () => { it("should handle PUT requests correctly", async () => { const testPayload = { title: "Updated Story" }; const response = new Response(JSON.stringify({ data: "test" }), { status: 200, headers: { "Content-Type": "application/json" }, }); mockFetch.mockResolvedValue(response); await sbFetch.put("stories/1", testPayload); expect(mockFetch).toHaveBeenCalledWith("https://api.storyblok.com/v2/stories/1", { method: "put", body: JSON.stringify(testPayload), headers: expect.any(Headers), signal: expect.any(AbortSignal), }); }); }); describe("patch", () => { it("should handle PATCH requests correctly", async () => { const testPayload = { winner_id: 42 } as ISbStoriesParams; const response = new Response(JSON.stringify({ data: "test" }), { status: 200, headers: { "Content-Type": "application/json" }, }); mockFetch.mockResolvedValue(response); await sbFetch.patch("stories/1/experiments/select_winner", testPayload); expect(mockFetch).toHaveBeenCalledWith( "https://api.storyblok.com/v2/stories/1/experiments/select_winner", { method: "patch", body: JSON.stringify(testPayload), headers: expect.any(Headers), signal: expect.any(AbortSignal), }, ); }); }); describe("delete", () => { it("should handle DELETE requests correctly", async () => { const response = new Response(null, { status: 204, // Typically, DELETE operations might not return content }); mockFetch.mockResolvedValue(response); await sbFetch.delete("stories/1"); expect(mockFetch).toHaveBeenCalledWith("https://api.storyblok.com/v2/stories/1", { method: "delete", body: "{}", // Ensuring no body is sent headers: expect.any(Headers), signal: expect.any(AbortSignal), }); }); }); it("should handle network errors gracefully", async () => { const mockFetch = vi.fn().mockRejectedValue(new Error("Network Failure")); const sbFetch = new SbFetch({ baseURL: "https://api.example.com", headers: new Headers(), fetch: mockFetch, }); // Assuming your implementation wraps the error message inside an object under `message`. const result = await sbFetch.get("/test", {}); // Check if the error object format matches your implementation. expect(result).toEqual({ message: expect.any(String), // Checks if `message` is a string }); // If you want to be more specific and check the message of the error: expect(result.message).toEqual("Network Failure"); // This path needs to match the structure you actually use. }); describe("timeout behavior", () => { // Helper to create mock fetch with configurable delay const createMockFetch = (delayMs: number) => { return vi.fn((_url: string, options?: any): Promise => { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { resolve(new Response(JSON.stringify({ data: "test" }), { status: 200 })); }, delayMs); options?.signal?.addEventListener("abort", () => { clearTimeout(timeoutId); const error = new Error("The operation was aborted"); error.name = "AbortError"; reject(error); }); }); }); }; // Helper to create SbFetch instance with timeout const createSbFetchWithTimeout = (timeoutSeconds: number, mockFetch: any) => { return new SbFetch({ baseURL: "https://api.storyblok.com/v2", timeout: timeoutSeconds, headers: new Headers(), fetch: mockFetch as any, }); }; it("should timeout after configured timeout period", async () => { const mockFetch = createMockFetch(5000); const sbFetch = createSbFetchWithTimeout(1, mockFetch); const result = await sbFetch.get("cdn/stories", {}); expect(result).toEqual({ message: "Request timeout: The request was aborted due to timeout", }); }, 3000); it("should timeout after 2 seconds when configured with 2s timeout", async () => { vi.useFakeTimers(); const mockFetch = createMockFetch(5000); const sbFetch = createSbFetchWithTimeout(2, mockFetch); const requestPromise = sbFetch.get("cdn/stories", {}); // After 1.9 seconds, request should still be pending await vi.advanceTimersByTimeAsync(1900); expect(mockFetch).toHaveBeenCalledTimes(1); // After 2+ seconds, should timeout await vi.advanceTimersByTimeAsync(200); const result = await requestPromise; expect(result).toEqual({ message: "Request timeout: The request was aborted due to timeout", }); vi.useRealTimers(); }, 3000); it("should timeout after 0.5 seconds when configured with 0.5s timeout", async () => { vi.useFakeTimers(); const mockFetch = createMockFetch(5000); const sbFetch = createSbFetchWithTimeout(0.5, mockFetch); const requestPromise = sbFetch.get("cdn/stories", {}); // After 0.5+ seconds, should timeout await vi.advanceTimersByTimeAsync(600); const result = await requestPromise; expect(result).toEqual({ message: "Request timeout: The request was aborted due to timeout", }); vi.useRealTimers(); }, 3000); it("should complete successfully if response arrives before timeout", async () => { const mockFetch = createMockFetch(500); // 500ms delay const sbFetch = createSbFetchWithTimeout(2, mockFetch); const result = await sbFetch.get("cdn/stories", {}); expect(result).toHaveProperty("data"); expect(result).toHaveProperty("status", 200); }, 3000); it("should not timeout when timeout is set to 0 (disabled)", async () => { const mockFetch = vi .fn() .mockResolvedValue(new Response(JSON.stringify({ data: "test" }), { status: 200 })); const sbFetch = createSbFetchWithTimeout(0, mockFetch); const result = await sbFetch.get("cdn/stories", {}); expect(result).toHaveProperty("data"); expect(result).toHaveProperty("status", 200); }, 3000); it("should return clear error message for timeout", async () => { const mockFetch = vi.fn((_url: string, options?: any): Promise => { return new Promise((_resolve, reject) => { options?.signal?.addEventListener("abort", () => { const error = new Error("The operation was aborted"); error.name = "AbortError"; reject(error); }); // Immediately abort to test the error message setTimeout(() => options?.signal?.dispatchEvent(new Event("abort")), 10); }); }); const sbFetch = createSbFetchWithTimeout(1, mockFetch); const result = await sbFetch.get("cdn/stories", {}); expect((result as any).message).toBe( "Request timeout: The request was aborted due to timeout", ); }); }); });