252 lines
8.2 KiB
TypeScript
252 lines
8.2 KiB
TypeScript
import { test, expect, describe } from "bun:test";
|
|
import {
|
|
normalizeTargetUrl,
|
|
stripRelayHeaders,
|
|
shouldSendBody,
|
|
buildRelayRequest,
|
|
createRelayResponse,
|
|
} from "./relay-utils";
|
|
|
|
describe("normalizeTargetUrl", () => {
|
|
test("returns null when target is null", () => {
|
|
expect(normalizeTargetUrl(null, "/")).toBeNull();
|
|
});
|
|
|
|
test("returns null when target is empty string", () => {
|
|
expect(normalizeTargetUrl("", "/")).toBeNull();
|
|
});
|
|
|
|
test("removes trailing slash from target", () => {
|
|
expect(normalizeTargetUrl("https://httpbin.org/", "/get")).toBe("https://httpbin.org/get");
|
|
});
|
|
|
|
test("keeps target without trailing slash", () => {
|
|
expect(normalizeTargetUrl("https://httpbin.org", "/get")).toBe("https://httpbin.org/get");
|
|
});
|
|
|
|
test("appends relay path", () => {
|
|
expect(normalizeTargetUrl("https://example.com", "/api/users")).toBe("https://example.com/api/users");
|
|
});
|
|
|
|
test("handles nested paths", () => {
|
|
expect(normalizeTargetUrl("https://api.example.com", "/v1/users/123/profile")).toBe(
|
|
"https://api.example.com/v1/users/123/profile"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("stripRelayHeaders", () => {
|
|
test("removes x-relay-target header", () => {
|
|
const headers = new Headers({ "x-relay-target": "https://test.com" });
|
|
const stripped = stripRelayHeaders(headers);
|
|
expect(stripped.get("x-relay-target")).toBeNull();
|
|
});
|
|
|
|
test("removes x-relay-path header", () => {
|
|
const headers = new Headers({ "x-relay-path": "/test" });
|
|
const stripped = stripRelayHeaders(headers);
|
|
expect(stripped.get("x-relay-path")).toBeNull();
|
|
});
|
|
|
|
test("removes host header", () => {
|
|
const headers = new Headers({ host: "localhost:3000" });
|
|
const stripped = stripRelayHeaders(headers);
|
|
expect(stripped.get("host")).toBeNull();
|
|
});
|
|
|
|
test("preserves other headers", () => {
|
|
const headers = new Headers({
|
|
"x-relay-target": "https://test.com",
|
|
"x-custom-header": "value",
|
|
authorization: "Bearer token",
|
|
});
|
|
const stripped = stripRelayHeaders(headers);
|
|
expect(stripped.get("x-custom-header")).toBe("value");
|
|
expect(stripped.get("authorization")).toBe("Bearer token");
|
|
});
|
|
|
|
test("handles multiple relay headers", () => {
|
|
const headers = new Headers({
|
|
"x-relay-target": "https://test.com",
|
|
"x-relay-path": "/path",
|
|
host: "test.com",
|
|
"content-type": "application/json",
|
|
});
|
|
const stripped = stripRelayHeaders(headers);
|
|
expect(stripped.get("content-type")).toBe("application/json");
|
|
expect(stripped.get("x-relay-target")).toBeNull();
|
|
expect(stripped.get("x-relay-path")).toBeNull();
|
|
expect(stripped.get("host")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("shouldSendBody", () => {
|
|
test("returns false for GET", () => {
|
|
expect(shouldSendBody("GET")).toBe(false);
|
|
});
|
|
|
|
test("returns false for HEAD", () => {
|
|
expect(shouldSendBody("HEAD")).toBe(false);
|
|
});
|
|
|
|
test("returns true for POST", () => {
|
|
expect(shouldSendBody("POST")).toBe(true);
|
|
});
|
|
|
|
test("returns true for PUT", () => {
|
|
expect(shouldSendBody("PUT")).toBe(true);
|
|
});
|
|
|
|
test("returns true for PATCH", () => {
|
|
expect(shouldSendBody("PATCH")).toBe(true);
|
|
});
|
|
|
|
test("returns true for DELETE", () => {
|
|
expect(shouldSendBody("DELETE")).toBe(true);
|
|
});
|
|
|
|
test("is case-sensitive", () => {
|
|
expect(shouldSendBody("get")).toBe(true);
|
|
expect(shouldSendBody("post")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildRelayRequest", () => {
|
|
test("sets correct method", () => {
|
|
const req = new Request("http://test.com", { method: "POST" });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.method).toBe("POST");
|
|
});
|
|
|
|
test("sets headers", () => {
|
|
const req = new Request("http://test.com");
|
|
const headers = new Headers({ "x-custom": "value" });
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.headers).toBe(headers);
|
|
});
|
|
|
|
test("omits body for GET", () => {
|
|
const req = new Request("http://test.com", { method: "GET" });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeUndefined();
|
|
});
|
|
|
|
test("omits body for HEAD", () => {
|
|
const req = new Request("http://test.com", { method: "HEAD" });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeUndefined();
|
|
});
|
|
|
|
test("includes body for POST", () => {
|
|
const body = JSON.stringify({ test: true });
|
|
const req = new Request("http://test.com", { method: "POST", body });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeDefined();
|
|
expect(result.duplex).toBe("half");
|
|
});
|
|
|
|
test("includes body for PUT", () => {
|
|
const body = JSON.stringify({ update: true });
|
|
const req = new Request("http://test.com", { method: "PUT", body });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeDefined();
|
|
});
|
|
|
|
test("includes body for PATCH", () => {
|
|
const body = JSON.stringify({ patch: true });
|
|
const req = new Request("http://test.com", { method: "PATCH", body });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeDefined();
|
|
});
|
|
|
|
test("includes body for DELETE", () => {
|
|
const body = JSON.stringify({ delete: true });
|
|
const req = new Request("http://test.com", { method: "DELETE", body });
|
|
const headers = new Headers();
|
|
const result = buildRelayRequest(req, "https://test.com", headers);
|
|
expect(result.body).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("createRelayResponse", () => {
|
|
test("preserves status", async () => {
|
|
const mockResponse = new Response("body", { status: 418 });
|
|
const result = createRelayResponse(mockResponse);
|
|
expect(result.status).toBe(418);
|
|
});
|
|
|
|
test("preserves headers", async () => {
|
|
const mockResponse = new Response("body", {
|
|
headers: { "x-custom": "value" },
|
|
});
|
|
const result = createRelayResponse(mockResponse);
|
|
expect(result.headers.get("x-custom")).toBe("value");
|
|
});
|
|
|
|
test("preserves body", async () => {
|
|
const mockResponse = new Response("test body content");
|
|
const result = createRelayResponse(mockResponse);
|
|
expect(await result.text()).toBe("test body content");
|
|
});
|
|
|
|
test("handles different status codes", async () => {
|
|
const mockResponse = new Response(null, { status: 404 });
|
|
const result = createRelayResponse(mockResponse);
|
|
expect(result.status).toBe(404);
|
|
});
|
|
|
|
test("handles 500 status", async () => {
|
|
const mockResponse = new Response("error", { status: 500 });
|
|
const result = createRelayResponse(mockResponse);
|
|
expect(result.status).toBe(500);
|
|
});
|
|
});
|
|
|
|
describe("integration: full relay flow", () => {
|
|
test("complete flow with mocked fetch", async () => {
|
|
const target = "https://httpbin.org";
|
|
const relayPath = "/get";
|
|
const targetUrl = normalizeTargetUrl(target, relayPath);
|
|
|
|
expect(targetUrl).toBe("https://httpbin.org/get");
|
|
|
|
const originalHeaders = new Headers({
|
|
"x-relay-target": target,
|
|
"x-relay-path": relayPath,
|
|
host: "localhost",
|
|
"x-custom": "preserved",
|
|
});
|
|
|
|
const strippedHeaders = stripRelayHeaders(originalHeaders);
|
|
expect(strippedHeaders.get("x-relay-target")).toBeNull();
|
|
expect(strippedHeaders.get("x-relay-path")).toBeNull();
|
|
expect(strippedHeaders.get("host")).toBeNull();
|
|
expect(strippedHeaders.get("x-custom")).toBe("preserved");
|
|
});
|
|
|
|
test("POST flow with body preservation", () => {
|
|
const req = new Request("http://test.com", {
|
|
method: "POST",
|
|
body: JSON.stringify({ data: "test" }),
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
|
|
const options = buildRelayRequest(req, "https://api.test.com/endpoint", new Headers());
|
|
expect(options.method).toBe("POST");
|
|
expect(options.body).toBeDefined();
|
|
});
|
|
|
|
test("error case: missing target", () => {
|
|
const target = null;
|
|
const relayPath = "/test";
|
|
const result = normalizeTargetUrl(target, relayPath);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|