mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createProviderFetch,
|
|
describeProviderError,
|
|
} from "../../../src/backend/providers/http.js";
|
|
import { AiProviderError } from "../../../src/backend/providers/types.js";
|
|
import { PRIVATE_DESTINATION_MESSAGE } from "../../../src/backend/egress.js";
|
|
|
|
describe("createProviderFetch", () => {
|
|
it("sends provider requests through ctx.fetch with the admin allowlist", async () => {
|
|
const response = new Response("{}");
|
|
const fetch = vi.fn(async () => response);
|
|
const controller = new AbortController();
|
|
const providerFetch = createProviderFetch(fetch, ["192.168.1.50"]);
|
|
|
|
await expect(
|
|
providerFetch("http://192.168.1.50:11434/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
signal: controller.signal,
|
|
}),
|
|
).resolves.toBe(response);
|
|
|
|
expect(fetch).toHaveBeenCalledWith(
|
|
"http://192.168.1.50:11434/api/chat",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: "{}",
|
|
signal: controller.signal,
|
|
allowPrivateHosts: ["192.168.1.50"],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("refuses a private address the admin has not allowed, before any request", async () => {
|
|
const fetch = vi.fn();
|
|
const providerFetch = createProviderFetch(fetch, ["localhost"]);
|
|
|
|
await expect(
|
|
providerFetch("http://10.0.0.9:11434/api/tags", { method: "GET" }),
|
|
).rejects.toThrow(PRIVATE_DESTINATION_MESSAGE);
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("explains a hostname that resolved to a private address", async () => {
|
|
const fetch = vi.fn(async () => {
|
|
throw new Error("Private destinations are not allowed");
|
|
});
|
|
const providerFetch = createProviderFetch(fetch, []);
|
|
|
|
await expect(
|
|
providerFetch("https://ollama.example.com/api/tags", { method: "GET" }),
|
|
).rejects.toThrow(PRIVATE_DESTINATION_MESSAGE);
|
|
});
|
|
|
|
it("passes other failures through unchanged", async () => {
|
|
const fetch = vi.fn(async () => {
|
|
throw new Error("socket hang up");
|
|
});
|
|
const providerFetch = createProviderFetch(fetch, []);
|
|
|
|
await expect(
|
|
providerFetch("https://api.openai.com/v1/models", { method: "GET" }),
|
|
).rejects.toThrow("socket hang up");
|
|
});
|
|
});
|
|
|
|
describe("describeProviderError", () => {
|
|
const undiciError = (message: string, cause?: unknown) =>
|
|
Object.assign(new TypeError(message), { cause });
|
|
|
|
it("explains a body that went quiet instead of a bare terminated", () => {
|
|
const error = undiciError(
|
|
"terminated",
|
|
Object.assign(new Error("Body Timeout Error"), {
|
|
code: "UND_ERR_BODY_TIMEOUT",
|
|
}),
|
|
);
|
|
expect(describeProviderError(error, "fallback")).toMatch(
|
|
/stopped sending data/,
|
|
);
|
|
});
|
|
|
|
it("explains a refused connection", () => {
|
|
const error = undiciError(
|
|
"fetch failed",
|
|
Object.assign(new Error("connect ECONNREFUSED"), {
|
|
code: "ECONNREFUSED",
|
|
}),
|
|
);
|
|
expect(describeProviderError(error, "fallback")).toMatch(/refused/);
|
|
});
|
|
|
|
it("maps a private address refused after DNS to the allowlist hint", () => {
|
|
const error = undiciError(
|
|
"fetch failed",
|
|
new Error("Private destinations are not allowed"),
|
|
);
|
|
expect(describeProviderError(error, "fallback")).toBe(
|
|
PRIVATE_DESTINATION_MESSAGE,
|
|
);
|
|
});
|
|
|
|
it("names the cause of an unfamiliar fetch failure", () => {
|
|
const error = undiciError("fetch failed", new Error("something odd"));
|
|
expect(describeProviderError(error, "fallback")).toBe(
|
|
"The provider request failed: something odd",
|
|
);
|
|
});
|
|
|
|
it("keeps a provider error's own message", () => {
|
|
expect(
|
|
describeProviderError(new AiProviderError("bad key", 401), "fallback"),
|
|
).toBe("bad key");
|
|
});
|
|
});
|