- Replace Next.js app router with standalone Bun.serve() entry point - Add middleware stack: rate limiter, body limiter, structured logger, SSRF protection - Add WebSocket bidirectional relay via x-relay-target header - Implement error classification (DNS→502, timeouts→504, SSRF→403, rate→429) - Remove all Next.js dependencies and config files - Update deploy workflow, tsconfig, wrangler config for Bun deployment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
3.9 KiB
TypeScript
138 lines
3.9 KiB
TypeScript
/**
|
|
* Integration tests for the relay proxy server handlers.
|
|
*
|
|
* Tests the exported route handler functions directly,
|
|
* bypassing the HTTP server layer.
|
|
*/
|
|
|
|
import { test, expect, describe, beforeAll, afterAll } from "bun:test";
|
|
|
|
// Import handler functions directly from index.ts
|
|
// Note: this will also start the Bun.serve() instance, which we allow.
|
|
import {
|
|
handleHealth,
|
|
handleDocs,
|
|
handleIndex,
|
|
getClientIP,
|
|
} from "./index";
|
|
|
|
describe("handleHealth", () => {
|
|
test("should return 200 with JSON body", async () => {
|
|
const response = handleHealth();
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("Content-Type")).toBe("application/json");
|
|
});
|
|
|
|
test("should include status, uptime, and version fields", async () => {
|
|
const response = handleHealth();
|
|
const body = await response.json();
|
|
expect(body.status).toBe("ok");
|
|
expect(typeof body.uptime).toBe("number");
|
|
expect(body.version).toBe("1.0.0");
|
|
});
|
|
|
|
test("should include CORS header", () => {
|
|
const response = handleHealth();
|
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
});
|
|
});
|
|
|
|
describe("handleDocs", () => {
|
|
test("should return 200 with HTML content", () => {
|
|
const response = handleDocs();
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("Content-Type")).toContain("text/html");
|
|
});
|
|
|
|
test("should include Edge Proxy Relay in the HTML", async () => {
|
|
const response = handleDocs();
|
|
const text = await response.text();
|
|
expect(text).toContain("Edge Proxy Relay");
|
|
expect(text).toContain("x-relay-target");
|
|
expect(text).toContain("WebSocket");
|
|
});
|
|
|
|
test("should include CORS header", () => {
|
|
const response = handleDocs();
|
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
|
});
|
|
});
|
|
|
|
describe("handleIndex", () => {
|
|
test("should return 200 with HTML content", () => {
|
|
const response = handleIndex();
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("Content-Type")).toContain("text/html");
|
|
});
|
|
|
|
test("should include Edge Proxy Relay title", async () => {
|
|
const response = handleIndex();
|
|
const text = await response.text();
|
|
expect(text).toContain("Edge Proxy Relay");
|
|
expect(text).toContain("Server is running");
|
|
});
|
|
});
|
|
|
|
describe("getClientIP", () => {
|
|
const mockIpGetter = (address: string) => ({
|
|
requestIP(_req: Request) {
|
|
return { address, family: "IPv4" as const, port: 12345 };
|
|
},
|
|
});
|
|
|
|
const nullIpGetter = {
|
|
requestIP(_req: Request) {
|
|
return null;
|
|
},
|
|
};
|
|
|
|
test("should return x-forwarded-for when present", () => {
|
|
const req = new Request("http://localhost/test", {
|
|
headers: { "x-forwarded-for": "198.51.100.1" },
|
|
});
|
|
const ip = getClientIP(req, nullIpGetter);
|
|
expect(ip).toBe("198.51.100.1");
|
|
});
|
|
|
|
test("should use first IP from x-forwarded-for list", () => {
|
|
const req = new Request("http://localhost/test", {
|
|
headers: {
|
|
"x-forwarded-for": "198.51.100.1, 203.0.113.5, 192.0.2.10",
|
|
},
|
|
});
|
|
const ip = getClientIP(req, nullIpGetter);
|
|
expect(ip).toBe("198.51.100.1");
|
|
});
|
|
|
|
test("should fall back to cf-connecting-ip", () => {
|
|
const req = new Request("http://localhost/test", {
|
|
headers: { "cf-connecting-ip": "203.0.113.50" },
|
|
});
|
|
const ip = getClientIP(req, nullIpGetter);
|
|
expect(ip).toBe("203.0.113.50");
|
|
});
|
|
|
|
test("should prefer x-forwarded-for over cf-connecting-ip", () => {
|
|
const req = new Request("http://localhost/test", {
|
|
headers: {
|
|
"x-forwarded-for": "198.51.100.1",
|
|
"cf-connecting-ip": "203.0.113.50",
|
|
},
|
|
});
|
|
const ip = getClientIP(req, nullIpGetter);
|
|
expect(ip).toBe("198.51.100.1");
|
|
});
|
|
|
|
test("should fall back to server.requestIP when no headers present", () => {
|
|
const req = new Request("http://localhost/test");
|
|
const ip = getClientIP(req, mockIpGetter("10.0.0.42"));
|
|
expect(ip).toBe("10.0.0.42");
|
|
});
|
|
|
|
test("should return unknown when nothing is available", () => {
|
|
const req = new Request("http://localhost/test");
|
|
const ip = getClientIP(req, nullIpGetter);
|
|
expect(ip).toBe("unknown");
|
|
});
|
|
});
|