380 lines · 14.0 KB · TypeScript
RawDownload
import { afterEach, beforeEach, describe, expect, it } from "@effect/vitest";import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { OAUTH_POPUP_MESSAGE_TYPE, type OAuthPopupResult } from "@executor-js/sdk"; import { acquireDataDirOwnership } from "./db/data-dir-ownership";import {  __oauthAwaitHeldWaiterTotalForTests,  __oauthAwaitWaiterCountForTests,  __resetOAuthResultStoreForTests,  consumeOAuthResult,  publishOAuthResult,} from "./oauth-result-store";import { startServer, type ServerInstance } from "./serve"; let clientDir: string;let dataDir: string;let server: ServerInstance | null = null; const TOKEN = "test-token"; const testHandlers = () => ({  api: {    handler: async () => new Response("ok"),    dispose: async () => {},  },  mcp: {    handleRequest: async () => new Response("ok"),    handleApprovalRequest: async () => new Response("ok"),    handlePausedRequest: async () => new Response("ok"),    close: async () => {},  },}); const startTestServer = async (  opts: { authToken?: string; hostname?: string } = {},): Promise<string> => {  server = await startServer({    port: 0,    hostname: opts.hostname ?? "127.0.0.1",    clientDir,    authToken: opts.authToken ?? TOKEN,    handlers: testHandlers(),  });  return `http://127.0.0.1:${server.port}`;}; beforeEach(() => {  clientDir = mkdtempSync(join(tmpdir(), "exec-local-serve-"));  dataDir = mkdtempSync(join(tmpdir(), "exec-local-data-"));  // Isolate auth.json writes and dynamic plugin lookup from the real ~/.executor/workspace.  process.env.EXECUTOR_DATA_DIR = dataDir;  process.env.EXECUTOR_SCOPE_DIR = dataDir;  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;  }  delete process.env.EXECUTOR_DATA_DIR;  delete process.env.EXECUTOR_SCOPE_DIR;  rmSync(clientDir, { recursive: true, force: true });  rmSync(dataDir, { recursive: true, force: true });}); describe("startServer static/SPA routing (unauthenticated)", () => {  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 without a token", async () => {    const baseUrl = await startTestServer();    // No Authorization header — the shell must still load so the browser can    // read its `?_token` and authenticate subsequent /api calls.    const response = await fetch(`${baseUrl}/integrations/add`);     expect(response.status).toBe(200);    expect(response.headers.get("content-type")).toContain("text/html");    expect(await response.text()).toContain("index-shell");  });}); describe("startServer startup cleanup", () => {  it("releases the owned DB when a default-handler server stops", async () => {    server = await startServer({ port: 0, clientDir, authToken: TOKEN });    await server.stop();    server = null;     const ownership = await acquireDataDirOwnership(dataDir);    // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: release the proof lock even if the assertion fails    try {      expect(ownership.lockPath).toContain("data.db.owner-lock");    } finally {      await ownership.release();    }  });   it("releases the owned DB if Bun.serve fails after handler boot", async () => {    const blocker = Bun.serve({      port: 0,      hostname: "127.0.0.1",      fetch: () => new Response("busy"),    });     // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: keep the busy-port blocker alive only for this assertion and always stop it    try {      await expect(        startServer({          port: blocker.port,          hostname: "127.0.0.1",          clientDir,          authToken: TOKEN,        }),      ).rejects.toThrow();       const ownership = await acquireDataDirOwnership(dataDir);      // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: release the proof lock even if the assertion fails      try {        expect(ownership.lockPath).toContain("data.db.owner-lock");      } finally {        await ownership.release();      }    } finally {      blocker.stop(true);    }  });}); describe("startServer bearer auth", () => {  it("mints and persists a 0600 auth.json when no token is supplied", async () => {    server = await startServer({ port: 0, clientDir, handlers: testHandlers() });    const tokenPath = join(dataDir, "server-control", "auth.json");    expect(existsSync(tokenPath)).toBe(true);    // Owner read/write only.    expect(statSync(tokenPath).mode & 0o777).toBe(0o600);    expect(typeof server.authToken).toBe("string");    expect(server.authToken.length).toBeGreaterThan(0);  });   it("serves /api/health without a token", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/api/health`);    expect(response.status).toBe(200);    expect(await response.text()).toBe("ok");  });   it("requires the bearer token on /api", async () => {    const baseUrl = await startTestServer();     const unauthorized = await fetch(`${baseUrl}/api/scope`);    expect(unauthorized.status).toBe(401);    expect(unauthorized.headers.get("www-authenticate")).toBe('Bearer realm="executor"');     const authorized = await fetch(`${baseUrl}/api/scope`, {      headers: { authorization: `Bearer ${TOKEN}` },    });    expect(authorized.status).toBe(200);    expect(await authorized.text()).toBe("ok");  });   it("requires the bearer token on /mcp", async () => {    const baseUrl = await startTestServer();     const unauthorized = await fetch(`${baseUrl}/mcp`, { method: "POST" });    expect(unauthorized.status).toBe(401);     const authorized = await fetch(`${baseUrl}/mcp`, {      method: "POST",      headers: { authorization: `Bearer ${TOKEN}` },    });    expect(authorized.status).toBe(200);  });   it("leaves the OAuth provider callback unauthenticated (state-gated)", async () => {    const baseUrl = await startTestServer();    // Reaches the api handler ("ok") rather than a 401 — the callback path is    // exempt because the external provider browser can't carry the bearer.    const response = await fetch(`${baseUrl}/api/oauth/callback?state=abc`);    expect(response.status).toBe(200);  });   it("serves the CIMD client metadata document without a bearer", async () => {    const baseUrl = await startTestServer();    const documentUrl = `${baseUrl}/api/oauth/client-id-metadata/local.json`;    const response = await fetch(documentUrl);    expect(response.status).toBe(200);    const metadata = (await response.json()) as {      readonly client_id: string;      readonly redirect_uris: readonly string[];      readonly application_type: string;    };    expect(metadata.client_id).toBe(documentUrl);    expect(metadata.redirect_uris).toEqual([      "http://127.0.0.1/api/oauth/callback",      "http://localhost/api/oauth/callback",      "http://[::1]/api/oauth/callback",    ]);    expect(metadata.application_type).toBe("native");  });   it("requires the bearer token on the OAuth await poll", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/api/oauth/await/session-1`);    expect(response.status).toBe(401);  });   it("auto-mints a token on a non-loopback bind instead of refusing", async () => {    server = await startServer({      port: 0,      hostname: "0.0.0.0",      clientDir,      handlers: testHandlers(),    });    expect(server.authToken.length).toBeGreaterThan(0);  });}); describe("startServer OAuth await long-poll", () => {  afterEach(() => {    __resetOAuthResultStoreForTests();  });   const untilWaiterCount = async (sessionId: string, count: number): Promise<void> => {    const deadline = Date.now() + 2000;    while (__oauthAwaitWaiterCountForTests(sessionId) !== count && Date.now() < deadline) {      await new Promise<void>((resolve) => setTimeout(resolve, 5));    }    expect(__oauthAwaitWaiterCountForTests(sessionId)).toBe(count);  };   const awaitResult = (sessionId: string): OAuthPopupResult<unknown> => ({    type: OAUTH_POPUP_MESSAGE_TYPE,    ok: false,    sessionId,    error: "access denied",  });   it("holds the await request open and answers the moment the result publishes", async () => {    const baseUrl = await startTestServer();     const pending = fetch(`${baseUrl}/api/oauth/await/session-hold`, {      headers: { authorization: `Bearer ${TOKEN}` },    });    // The request is being held, not answered null immediately.    await untilWaiterCount("session-hold", 1);     const publishedAt = Date.now();    publishOAuthResult(awaitResult("session-hold"));    const response = await pending;    const body = (await response.json()) as unknown;     // Resolved by the publish, not a poll tick or the 25s deadline.    expect(Date.now() - publishedAt).toBeLessThan(1000);    expect(body).toMatchObject({ sessionId: "session-hold", ok: false });    expect(__oauthAwaitWaiterCountForTests("session-hold")).toBe(0);  });   it("answers a pre-published result without waiting", async () => {    const baseUrl = await startTestServer();    publishOAuthResult(awaitResult("session-ready"));     const response = await fetch(`${baseUrl}/api/oauth/await/session-ready`, {      headers: { authorization: `Bearer ${TOKEN}` },    });    const body = (await response.json()) as unknown;     expect(body).toMatchObject({ sessionId: "session-ready" });    // One-shot: a second poll for the same session finds nothing.    expect(consumeOAuthResult("session-ready")).toBeNull();  });   it("drops the waiter when the client disconnects", async () => {    const baseUrl = await startTestServer();     const controller = new AbortController();    const pending = fetch(`${baseUrl}/api/oauth/await/session-gone`, {      headers: { authorization: `Bearer ${TOKEN}` },      signal: controller.signal,    });    await untilWaiterCount("session-gone", 1);     controller.abort();    await expect(pending).rejects.toThrow();    await untilWaiterCount("session-gone", 0);     // The dead waiter must not consume a result published afterwards.    publishOAuthResult(awaitResult("session-gone"));    expect(consumeOAuthResult("session-gone")).toMatchObject({ sessionId: "session-gone" });  });   it("holds one request per session; stacked requests answer null instantly and one consumer wins", async () => {    // The mixed-version rollout hazard: an unpatched desktop client polls    // every second and would stack ~25 concurrent requests per flow against    // a holding server. Only the first request may hold — the rest must get    // the pre-long-poll instant "still pending" answer.    const baseUrl = await startTestServer();     const held = fetch(`${baseUrl}/api/oauth/await/session-stacked`, {      headers: { authorization: `Bearer ${TOKEN}` },    });    // Deterministic ordering: the first request is registered before any    // stacked request is issued.    await untilWaiterCount("session-stacked", 1);     const stacked = await Promise.all(      [1, 2, 3].map(() =>        fetch(`${baseUrl}/api/oauth/await/session-stacked`, {          headers: { authorization: `Bearer ${TOKEN}` },        }),      ),    );    // The stacked requests settled BEFORE any publish — instant null answers,    // not held connections.    for (const response of stacked) {      expect(response.status).toBe(200);      expect(await response.json()).toBeNull();    }    expect(__oauthAwaitWaiterCountForTests("session-stacked")).toBe(1);    expect(__oauthAwaitHeldWaiterTotalForTests()).toBe(1);     publishOAuthResult(awaitResult("session-stacked"));    const response = await held;    // Exactly one consumer overall receives the one-shot result: the held    // request. Nothing is left behind for a later poll.    expect(await response.json()).toMatchObject({ sessionId: "session-stacked" });    expect(consumeOAuthResult("session-stacked")).toBeNull();    // The registry drains completely — no leaked waiters.    expect(__oauthAwaitWaiterCountForTests("session-stacked")).toBe(0);    expect(__oauthAwaitHeldWaiterTotalForTests()).toBe(0);  });}); describe("startServer CORS hardening", () => {  it("reflects credentialed CORS only for allowed loopback origins", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/api/health`, {      headers: { origin: "http://127.0.0.1:4789" },    });    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");  });   it("does not send CORS headers to a disallowed origin", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/api/health`, {      headers: { origin: "https://evil.example" },    });    expect(response.headers.get("access-control-allow-origin")).toBeNull();  });   it("answers browser CORS preflights before auth", async () => {    const baseUrl = await startTestServer();    const response = await fetch(`${baseUrl}/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");  });});