aegida-console / lib / chat / gate-files.test.ts
gate-files.test.ts
Raw
// @vitest-environment node

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("server-only", () => ({}));

import {
  deleteGateFile,
  GateFileError,
  getGateFileContent,
  uploadGateFile,
} from "@/lib/chat/gate-files";

beforeEach(() => {
  vi.stubEnv("AEGIDA_GATE_URL", "https://gate.example.test");
});

afterEach(() => {
  vi.unstubAllEnvs();
  vi.unstubAllGlobals();
});

describe("Aegida Gate Files client", () => {
  it("streams the original File with purpose user_data and the exact user token", async () => {
    const file = new File(["brief"], "brief.txt", { type: "text/plain" });
    const arrayBuffer = vi.spyOn(file, "arrayBuffer").mockRejectedValue(
      new Error("whole-file-arrayBuffer-must-not-be-used"),
    );
    const fetchMock = vi.fn().mockResolvedValue(
      Response.json({
        id: "file-opaque_123",
        object: "file",
        bytes: 5,
        created_at: 1_785_758_400,
        filename: "brief.txt",
        purpose: "user_data",
      }),
    );
    vi.stubGlobal("fetch", fetchMock);
    const signal = new AbortController().signal;

    await expect(uploadGateFile(file, "Bearer exact-user-jwt", signal)).resolves.toEqual({
      id: "file-opaque_123",
      filename: "brief.txt",
      bytes: 5,
    });

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe("https://gate.example.test/v1/files");
    expect(init).toMatchObject({
      method: "POST",
      headers: { Authorization: "Bearer exact-user-jwt" },
      cache: "no-store",
      redirect: "error",
      signal,
    });
    expect(init.headers).not.toHaveProperty("Content-Type");
    expect(init.body).toBeInstanceOf(FormData);
    expect((init.body as FormData).get("purpose")).toBe("user_data");
    expect((init.body as FormData).get("file")).toBe(file);
    expect(arrayBuffer).not.toHaveBeenCalled();
  });

  it.each([
    null,
    {},
    { id: "not-a-file", object: "file", bytes: 5, created_at: 1, filename: "a.txt", purpose: "user_data" },
    { id: "file-good", object: "other", bytes: 5, created_at: 1, filename: "a.txt", purpose: "user_data" },
    { id: "file-good", object: "file", bytes: -1, created_at: 1, filename: "a.txt", purpose: "user_data" },
    { id: "file-good", object: "file", bytes: 5, created_at: 1, filename: "", purpose: "user_data" },
    { id: "file-good", object: "file", bytes: 5, created_at: 1, filename: "a.txt", purpose: "assistants" },
  ])("rejects a malformed FileObject %#", async (value) => {
    vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json(value)));

    await expect(
      uploadGateFile(
        new File(["brief"], "brief.txt", { type: "text/plain" }),
        "Bearer user",
        new AbortController().signal,
      ),
    ).rejects.toBeInstanceOf(GateFileError);
  });

  it("uses authenticated standard content and delete paths", async () => {
    const fetchMock = vi
      .fn()
      .mockResolvedValueOnce(new Response("private file contents"))
      .mockResolvedValueOnce(new Response(null, { status: 204 }));
    vi.stubGlobal("fetch", fetchMock);
    const signal = new AbortController().signal;

    const content = await getGateFileContent("file-opaque_123", "Bearer user", signal);
    expect(await new Response(content).text()).toBe("private file contents");
    await deleteGateFile("file-opaque_123", "Bearer user", signal);

    expect(fetchMock).toHaveBeenNthCalledWith(1, "https://gate.example.test/v1/files/file-opaque_123/content", {
      method: "GET",
      headers: { Authorization: "Bearer user" },
      cache: "no-store",
      redirect: "error",
      signal,
    });
    expect(fetchMock).toHaveBeenNthCalledWith(2, "https://gate.example.test/v1/files/file-opaque_123", {
      method: "DELETE",
      headers: { Authorization: "Bearer user" },
      cache: "no-store",
      redirect: "error",
      signal,
    });
  });

  it("sanitizes non-2xx and oversized JSON responses", async () => {
    vi.stubGlobal(
      "fetch",
      vi
        .fn()
        .mockResolvedValueOnce(new Response("private-dependency-sentinel", { status: 500 }))
        .mockResolvedValueOnce(new Response("x".repeat(1_048_577))),
    );
    const file = new File(["brief"], "brief.txt", { type: "text/plain" });

    for (let attempt = 0; attempt < 2; attempt += 1) {
      let error: unknown;
      try {
        await uploadGateFile(file, "Bearer user", new AbortController().signal);
      } catch (caught) {
        error = caught;
      }
      expect(error).toBeInstanceOf(GateFileError);
      expect(String(error)).not.toContain("private-dependency-sentinel");
      expect(String(error)).not.toContain("brief");
    }
  });
});