193 lines · 6.0 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { startServer, type ServerInstance } from "./serve"; let clientDir: string;let server: ServerInstance | null = null; const startTestServer = async (): Promise<string> => {  server = await startServer({    port: 0,    hostname: "127.0.0.1",    clientDir,    handlers: {      api: {        handler: async () => new Response("ok"),        dispose: async () => {},      },      mcp: {        handleRequest: async () => new Response("ok"),        handleApprovalRequest: async () => new Response("ok"),        close: async () => {},      },    },  });  return `http://127.0.0.1:${server.port}`;}; beforeEach(() => {  clientDir = mkdtempSync(join(tmpdir(), "exec-local-serve-"));  mkdirSync(join(clientDir, "assets"), { recursive: true });  writeFileSync(    join(clientDir, "index.html"),    "<!doctype html><html><body>index-shell</body></html>",  );  writeFileSync(join(clientDir, "assets", "app.js"), "console.log('ok')");}); afterEach(async () => {  if (server) {    await server.stop();    server = null;  }  rmSync(clientDir, { recursive: true, force: true });}); describe("startServer static/SPA routing", () => {  it("returns 404 for missing asset-like paths", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/assets/missing.js`);     expect(response.status).toBe(404);    expect(await response.text()).toBe("Not Found");  });   it("falls back to index.html for extension-less SPA routes", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/sources/add`);     expect(response.status).toBe(200);    expect(response.headers.get("content-type")).toContain("text/html");    expect(await response.text()).toContain("index-shell");  });}); describe("startServer network bind auth", () => {  it("refuses non-loopback binds without a token or password", async () => {    await expect(      startServer({        port: 0,        hostname: "0.0.0.0",        clientDir,        handlers: {          api: {            handler: async () => new Response("ok"),            dispose: async () => {},          },          mcp: {            handleRequest: async () => new Response("ok"),            handleApprovalRequest: async () => new Response("ok"),            close: async () => {},          },        },      }),    ).rejects.toThrow("non-loopback host without an auth token or password");  });   it("requires the configured token when auth is enabled", async () => {    server = await startServer({      port: 0,      hostname: "127.0.0.1",      clientDir,      authToken: "test-token",      handlers: {        api: {          handler: async () => new Response("ok"),          dispose: async () => {},        },        mcp: {          handleRequest: async () => new Response("ok"),          handleApprovalRequest: async () => new Response("ok"),          close: async () => {},        },      },    });     const baseUrl = `http://127.0.0.1:${server.port}`;    const unauthorized = await fetch(`${baseUrl}/api/health`);    expect(unauthorized.status).toBe(401);     const authorized = await fetch(`${baseUrl}/api/health`, {      headers: { authorization: "Bearer test-token" },    });    expect(authorized.status).toBe(200);    expect(await authorized.text()).toBe("ok");  });   it("answers browser CORS preflights before auth", async () => {    server = await startServer({      port: 0,      hostname: "127.0.0.1",      clientDir,      authToken: "test-token",      handlers: {        api: {          handler: async () => new Response("ok"),          dispose: async () => {},        },        mcp: {          handleRequest: async () => new Response("ok"),          handleApprovalRequest: async () => new Response("ok"),          close: async () => {},        },      },    });     const response = await fetch(`http://127.0.0.1:${server.port}/api/scope`, {      method: "OPTIONS",      headers: {        origin: "http://127.0.0.1:4789",        "access-control-request-method": "GET",        "access-control-request-headers": "authorization,b3,traceparent",      },    });     expect(response.status).toBe(204);    expect(response.headers.get("access-control-allow-origin")).toBe("http://127.0.0.1:4789");    expect(response.headers.get("access-control-allow-credentials")).toBe("true");    expect(response.headers.get("access-control-allow-headers")).toContain("traceparent");    expect(response.headers.get("access-control-allow-headers")).toContain("b3");  });   it("adds CORS headers to authenticated API failures", async () => {    server = await startServer({      port: 0,      hostname: "127.0.0.1",      clientDir,      authToken: "test-token",      handlers: {        api: {          handler: async () => new Response("ok"),          dispose: async () => {},        },        mcp: {          handleRequest: async () => new Response("ok"),          handleApprovalRequest: async () => new Response("ok"),          close: async () => {},        },      },    });     const response = await fetch(`http://127.0.0.1:${server.port}/api/scope`, {      headers: { origin: "http://127.0.0.1:4789" },    });     expect(response.status).toBe(401);    expect(response.headers.get("access-control-allow-origin")).toBe("http://127.0.0.1:4789");    expect(response.headers.get("access-control-allow-credentials")).toBe("true");    expect(response.headers.get("access-control-allow-headers")).toContain("traceparent");  });   it("does not expose unauthenticated loopback servers cross-origin", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/api/scope`, {      headers: { origin: "https://example.com" },    });     expect(response.status).toBe(200);    expect(response.headers.get("access-control-allow-origin")).toBeNull();  });});