224 lines · 9.5 KB · TypeScript
RawDownload
// Shared HTTP test harness for node-pool integration tests.//// Stands up the real ProtectedCloudApi against a real DbService and// every real plugin (openapi / mcp / graphql / workos-vault), with// two test-only swaps:////   - Auth is faked: the executor binds `{ tenant, subject }` read off the//     `x-test-org-id` / `x-test-user-id` headers instead of the WorkOS cookie.//   - `workos-vault` is configured with an in-memory `WorkOSVaultClient`//     so connection writes never reach WorkOS's real API.//// Tests get a `fetchForOrg(organizationId)` they can hand to `FetchHttpClient`// and then call `HttpApiClient.make(ProtectedCloudApi)` against it.// Each test picks its own org id (usually a random UUID) so rows don't// collide across tests.//// v2: the executor is bound to a tenant (the organization id) and a subject// (the account id). The org-shared catalog is `owner: "org"`; a member's own// connections are `owner: "user"`. There is no scope stack and no scope id. import { Effect, Layer } from "effect";import { HttpApiBuilder, HttpApiClient, HttpApiSwagger } from "effect/unstable/httpapi";import { FetchHttpClient, HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http"; import {  ExecutionEngineService,  ExecutorService,  collectTables,  providePluginExtensions,  type PluginExtensionServices,} from "@executor-js/api/server";import { createExecutionEngine } from "@executor-js/execution";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { createExecutor, Subject, Tenant } from "@executor-js/sdk";import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing"; import executorConfig from "../../executor.config";import { AuthContext, RouterConfigLive } from "@executor-js/api/server"; import { ProtectedCloudApi, ProtectedCloudApiHandlers } from "../api/layers";import { DbService } from "../db/db";import { createDrizzleFumaDb } from "../db/fuma"; export const TEST_BASE_URL = "http://test.local";export const TEST_ORG_HEADER = "x-test-org-id";export const TEST_USER_HEADER = "x-test-user-id"; // `asOrg(organizationId, …)` callers don't care which specific user they are,// only that the executor has a bound subject so `owner: "user"` operations work.// We give each org a stable default subject so list/get operations remain// deterministic across calls within a single test.const defaultUserFor = (organizationId: string) => `default_user_${organizationId}`; // ---------------------------------------------------------------------------// Executor factory — mirrors `makeScopedExecutor` (binds `{ tenant, subject }`)// but with an in-memory test vault client (see// `@executor-js/plugin-workos-vault/testing`).// --------------------------------------------------------------------------- const fakeVault = makeTestWorkOSVaultClient();const testPlugins = executorConfig.plugins({  workosVaultClient: fakeVault,});const testHttpClientLayer = FetchHttpClient.layer; const createTestScopedExecutor = (userId: string, organizationId: string) =>  Effect.gen(function* () {    const { db } = yield* DbService;    const plugins = testPlugins;    const fuma = createDrizzleFumaDb({      db,      tables: collectTables(),      namespace: "executor_cloud",      provider: "postgresql",    });    return yield* createExecutor({      tenant: Tenant.make(organizationId),      subject: Subject.make(userId),      db: fuma.db,      plugins,      httpClientLayer: testHttpClientLayer,      onElicitation: "accept-all",      // EXPLICIT OAuth callback — production derives      // `${webBaseUrl}${CLOUD_MOUNT_PREFIX}/oauth/callback` in `makeScopedExecutor`      // (the cloud mounts the API under `/api`); the harness wires the matching      // stable test equivalent so the OAuth `start` (authorization_code) flow      // returns a redirect instead of failing loudly on the now-required redirectUri.      redirectUri: "https://test.executor.sh/api/oauth/callback",    });  }); // ---------------------------------------------------------------------------// HTTP plumbing// --------------------------------------------------------------------------- // Test version of the production `ExecutionStackMiddleware` — reads the// `x-test-org-id` (and optional `x-test-user-id`) header, builds a// test-scoped executor against the live postgres test db with a fake// WorkOS vault, and provides `AuthContext` + the executor services to the// handler. Mirrors prod's HttpRouter middleware but with test-mode// constructors.const TestExecutionStackMiddleware = HttpRouter.middleware<{  provides:    | AuthContext    | ExecutorService    | ExecutionEngineService    | PluginExtensionServices<typeof testPlugins>;}>()(  // Layer-time setup — captures `DbService` so the per-request function  // only depends on `HttpRouter`-Provided context. See `api/protected.ts`  // for the same pattern.  Effect.gen(function* () {    const context = yield* Effect.context<DbService>();    const provideExecutorExtensions = providePluginExtensions(testPlugins);    return (httpEffect) =>      Effect.gen(function* () {        const request = yield* HttpServerRequest.HttpServerRequest;        const organizationId = request.headers[TEST_ORG_HEADER];        if (!organizationId || typeof organizationId !== "string") {          // oxlint-disable-next-line executor/no-effect-escape-hatch, executor/no-error-constructor -- boundary: test HTTP harness has no request context without x-test-org-id          return yield* Effect.die(new Error("missing x-test-org-id"));        }        const userHeader = request.headers[TEST_USER_HEADER];        const userId =          typeof userHeader === "string" && userHeader.length > 0            ? userHeader            : defaultUserFor(organizationId);        const executor = yield* createTestScopedExecutor(userId, organizationId);        const engine = createExecutionEngine({          executor,          codeExecutor: makeQuickJsExecutor(),        });        return yield* httpEffect.pipe(          Effect.provideService(            AuthContext,            AuthContext.of({              accountId: userId,              organizationId,              email: "test@example.com",              name: "Test User",              avatarUrl: null,              roles: [],            }),          ),          Effect.provideService(ExecutorService, executor),          Effect.provideService(ExecutionEngineService, engine),          provideExecutorExtensions(executor),        );      }).pipe(Effect.provideContext(context));  }),).layer; const TestApiLive = HttpApiBuilder.layer(ProtectedCloudApi).pipe(  Layer.provide(ProtectedCloudApiHandlers),  Layer.provide(TestExecutionStackMiddleware),  Layer.provideMerge(HttpApiSwagger.layer(ProtectedCloudApi, { path: "/docs" })),  Layer.provideMerge(RouterConfigLive),  Layer.provideMerge(DbService.Live),  Layer.provideMerge(HttpServer.layerServices),); const handler = HttpRouter.toWebHandler(TestApiLive, { disableLogger: true }).handler; export const fetchForOrg = (organizationId: string): typeof globalThis.fetch =>  ((input: RequestInfo | URL, init?: RequestInit) => {    const base = input instanceof Request ? input : new Request(input, init);    const req = new Request(base, {      headers: { ...Object.fromEntries(base.headers), [TEST_ORG_HEADER]: organizationId },    });    return handler(req);  }) as typeof globalThis.fetch; export const fetchForUser = (userId: string, organizationId: string): typeof globalThis.fetch =>  ((input: RequestInfo | URL, init?: RequestInit) => {    const base = input instanceof Request ? input : new Request(input, init);    const req = new Request(base, {      headers: {        ...Object.fromEntries(base.headers),        [TEST_ORG_HEADER]: organizationId,        [TEST_USER_HEADER]: userId,      },    });    return handler(req);  }) as typeof globalThis.fetch; export const clientLayerForOrg = (organizationId: string) =>  FetchHttpClient.layer.pipe(    Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchForOrg(organizationId))),  ); export const clientLayerForUser = (userId: string, organizationId: string) =>  FetchHttpClient.layer.pipe(    Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchForUser(userId, organizationId))),  ); // Constructs an HttpApiClient bound to the given org, hands it to `body`,// and provides the org-scoped fetch layer in one step. Keeps per-test// Effect blocks focused on the actual assertions.type ApiShape = HttpApiClient.ForApi<typeof ProtectedCloudApi>; export const asOrg = <A, E>(  organizationId: string,  body: (client: ApiShape) => Effect.Effect<A, E>,): Effect.Effect<A, E> =>  Effect.gen(function* () {    const client = yield* HttpApiClient.make(ProtectedCloudApi, { baseUrl: TEST_BASE_URL });    return yield* body(client);  }).pipe(Effect.provide(clientLayerForOrg(organizationId))) as Effect.Effect<A, E>; // Same as `asOrg` but also threads a specific user id through the fake auth, so// the built executor's bound subject is `userId`. Use this for tests that care// about per-user isolation (`owner: "user"` connections) inside the same org.export const asUser = <A, E>(  userId: string,  organizationId: string,  body: (client: ApiShape) => Effect.Effect<A, E>,): Effect.Effect<A, E> =>  Effect.gen(function* () {    const client = yield* HttpApiClient.make(ProtectedCloudApi, { baseUrl: TEST_BASE_URL });    return yield* body(client);  }).pipe(Effect.provide(clientLayerForUser(userId, organizationId))) as Effect.Effect<A, E>; // Re-exports so call sites don't need a second import.export { ProtectedCloudApi };