import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { apiRequest, apiGet, apiPost, apiPatch, apiDelete } from "./client"; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch; describe("API Client", () => { beforeEach(() => { mockFetch.mockClear(); }); afterEach(() => { vi.resetAllMocks(); }); describe("apiRequest", () => { it("should make a successful GET request", async () => { const mockData = { id: "1", name: "Test" }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockData, }); const result = await apiRequest("/test"); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test", expect.objectContaining({ headers: expect.objectContaining({ "Content-Type": "application/json", }), credentials: "include", }) ); expect(result).toEqual(mockData); }); it("should include custom headers", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}), }); await apiRequest("/test", { headers: { Authorization: "Bearer token123" }, }); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test", expect.objectContaining({ headers: expect.objectContaining({ "Content-Type": "application/json", Authorization: "Bearer token123", }), }) ); }); it("should throw error on failed request", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Not Found", json: async () => ({ code: "NOT_FOUND", message: "Resource not found", }), }); await expect(apiRequest("/test")).rejects.toThrow("Resource not found"); }); it("should handle errors when JSON parsing fails", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Internal Server Error", json: async () => { throw new Error("Invalid JSON"); }, }); await expect(apiRequest("/test")).rejects.toThrow( "Internal Server Error" ); }); }); describe("apiGet", () => { it("should make a GET request", async () => { const mockData = { id: "1" }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockData, }); const result = await apiGet("/test"); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test", expect.objectContaining({ method: "GET" }) ); expect(result).toEqual(mockData); }); }); describe("apiPost", () => { it("should make a POST request with data", async () => { const postData = { name: "New Item" }; const mockResponse = { id: "1", ...postData }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); const result = await apiPost("/test", postData); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test", expect.objectContaining({ method: "POST", body: JSON.stringify(postData), }) ); expect(result).toEqual(mockResponse); }); it("should make a POST request without data", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({}), }); await apiPost("/test"); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test", expect.objectContaining({ method: "POST", // When no data is provided, body property is not set (not undefined) }) ); // Verify body is not in the call const callArgs = mockFetch.mock.calls[0][1] as RequestInit; expect(callArgs.body).toBeUndefined(); }); }); describe("apiPatch", () => { it("should make a PATCH request with data", async () => { const patchData = { name: "Updated" }; const mockResponse = { id: "1", ...patchData }; mockFetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse, }); const result = await apiPatch("/test/1", patchData); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test/1", expect.objectContaining({ method: "PATCH", body: JSON.stringify(patchData), }) ); expect(result).toEqual(mockResponse); }); }); describe("apiDelete", () => { it("should make a DELETE request", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }), }); const result = await apiDelete<{ success: boolean }>("/test/1"); expect(mockFetch).toHaveBeenCalledWith( "http://localhost:3001/test/1", expect.objectContaining({ method: "DELETE" }) ); expect(result).toEqual({ success: true }); }); }); describe("error handling", () => { it("should handle network errors", async () => { mockFetch.mockRejectedValueOnce(new Error("Network request failed")); await expect(apiGet("/test")).rejects.toThrow("Network request failed"); }); it("should handle 401 unauthorized errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Unauthorized", status: 401, json: async () => ({ code: "UNAUTHORIZED", message: "Authentication required", }), }); await expect(apiGet("/test")).rejects.toThrow("Authentication required"); }); it("should handle 403 forbidden errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Forbidden", status: 403, json: async () => ({ code: "FORBIDDEN", message: "Access denied", }), }); await expect(apiGet("/test")).rejects.toThrow("Access denied"); }); it("should handle 404 not found errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Not Found", status: 404, json: async () => ({ code: "NOT_FOUND", message: "Resource not found", }), }); await expect(apiGet("/test")).rejects.toThrow("Resource not found"); }); it("should handle 500 server errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Internal Server Error", status: 500, json: async () => ({ code: "INTERNAL_ERROR", message: "Internal server error", }), }); await expect(apiGet("/test")).rejects.toThrow("Internal server error"); }); it("should handle malformed JSON responses", async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => { throw new Error("Unexpected token in JSON"); }, }); await expect(apiGet("/test")).rejects.toThrow("Unexpected token in JSON"); }); it("should handle empty error responses", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Bad Request", status: 400, json: async () => { throw new Error("No JSON body"); }, }); await expect(apiGet("/test")).rejects.toThrow("Bad Request"); }); it("should handle timeout errors", async () => { mockFetch.mockImplementationOnce(() => { return new Promise((_, reject) => { setTimeout(() => reject(new Error("Request timeout")), 1); }); }); await expect(apiGet("/test")).rejects.toThrow("Request timeout"); }); it("should handle malformed error responses with details", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Validation Error", status: 422, json: async () => ({ code: "VALIDATION_ERROR", message: "Invalid input", details: { fields: { email: "Invalid email format", password: "Password too short", }, }, }), }); await expect(apiGet("/test")).rejects.toThrow("Invalid input"); }); it("should handle CORS errors", async () => { mockFetch.mockRejectedValueOnce( new TypeError("Failed to fetch") ); await expect(apiGet("/test")).rejects.toThrow("Failed to fetch"); }); it("should handle rate limit errors", async () => { mockFetch.mockResolvedValueOnce({ ok: false, statusText: "Too Many Requests", status: 429, json: async () => ({ code: "RATE_LIMIT_EXCEEDED", message: "Too many requests. Please try again later.", }), }); await expect(apiGet("/test")).rejects.toThrow( "Too many requests. Please try again later." ); }); it("should handle connection refused errors", async () => { mockFetch.mockRejectedValueOnce({ name: "FetchError", message: "request to http://localhost:3001/test failed, reason: connect ECONNREFUSED", }); await expect(apiGet("/test")).rejects.toMatchObject({ message: expect.stringContaining("ECONNREFUSED"), }); }); }); });