Newer
Older
smart-home-server / webclient-vue / src / api / __tests__ / http.spec.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 23 hours ago 4 KB Add script detail pages with scope grouping
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");
  });
});