import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { requestHttp } from "../http.js";
describe("requestHttp", () => {
beforeEach(() => {
vi.stubEnv("VITE_API_BASE_URL", "");
vi.stubEnv("VITE_API_PROXY_PATH", "/proxy.php");
vi.stubEnv("VITE_API_TIMEOUT_MS", "10000");
global.fetch = vi.fn();
});
afterEach(() => {
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it("builds proxy URL when VITE_API_PROXY_PATH is set", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/api/v1/areas/list");
expect(fetch).toHaveBeenCalledOnce();
const url = fetch.mock.calls[0][0];
expect(url).toBe("/proxy.php?path=%2Fapi%2Fv1%2Fareas%2Flist");
});
it("appends query params to proxy URL", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/api/v1/areas/list", null, { query: { page: 2 } });
const url = fetch.mock.calls[0][0];
expect(url).toContain("path=%2Fapi%2Fv1%2Fareas%2Flist");
expect(url).toContain("page=2");
});
it("uses direct URL when proxy path is empty", async () => {
vi.stubEnv("VITE_API_PROXY_PATH", "");
vi.stubEnv("VITE_API_BASE_URL", "http://server.local");
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/api/v1/areas/list");
const url = fetch.mock.calls[0][0];
expect(url).toBe("http://server.local/api/v1/areas/list");
});
it("sends JSON body with Content-Type header", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("POST", "/api/v1/areas/new-area", { name: "Kitchen" });
const init = fetch.mock.calls[0][1];
expect(init.method).toBe("POST");
expect(init.headers["Content-Type"]).toBe("application/json");
expect(init.body).toBe('{"name":"Kitchen"}');
});
it("includes Accept header", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/test");
const init = fetch.mock.calls[0][1];
expect(init.headers.Accept).toBe("application/json");
});
it("passes custom headers", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/test", null, { headers: { "X-Custom": "value" } });
const init = fetch.mock.calls[0][1];
expect(init.headers["X-Custom"]).toBe("value");
});
it("passes AbortController signal to fetch", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true}',
headers: new Headers(),
});
await requestHttp("GET", "/test");
const init = fetch.mock.calls[0][1];
expect(init.signal).toBeInstanceOf(AbortSignal);
});
it("parses JSON response", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => '{"status":true,"data":{"items":[]}}',
headers: new Headers(),
});
const result = await requestHttp("GET", "/test");
expect(result.data).toEqual({ status: true, data: { items: [] } });
});
it("returns raw text for non-JSON response", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => "plain text",
headers: new Headers(),
});
const result = await requestHttp("GET", "/test");
expect(result.data).toBe("plain text");
});
it("returns meta with url, method, statusCode", async () => {
fetch.mockResolvedValue({
status: 200,
text: async () => "{}",
headers: new Headers(),
});
const result = await requestHttp("POST", "/api/v1/test", { a: 1 });
expect(result.meta.method).toBe("POST");
expect(result.meta.statusCode).toBe(200);
expect(result.meta.url).toContain("path=%2Fapi%2Fv1%2Ftest");
});
});