1453 lines · 46.7 KB · TypeScript
RawDownload
import { expect, layer } from "@effect/vitest";import { Effect, Layer, Predicate, Schema } from "effect";import {  HttpApi,  HttpApiBuilder,  HttpApiEndpoint,  HttpApiGroup,  OpenApi,} from "effect/unstable/httpapi";import { HttpClient, HttpRouter, HttpServerRequest } from "effect/unstable/http";import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";import http from "node:http";import type { AddressInfo } from "node:net"; import {  createExecutor,  definePlugin,  type DBAdapter,  makeTestConfig,  RemoveSecretInput,  Scope,  ScopeId,  SecretId,  SetSecretInput,  type InvokeOptions,  type SecretProvider,  type Where,} from "@executor-js/sdk";import { memorySecretsPlugin } from "@executor-js/sdk/testing";import type { ConfigFileSink } from "@executor-js/config"; const TEST_SCOPE = "test-scope";import { openApiPlugin } from "./plugin";import { ConfiguredHeaderBinding, OAuth2SourceConfig, OpenApiSourceBindingInput } from "./types";import { makeOpenApiTestServer } from "../testing"; const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; type FindManyCall = {  readonly model: string;  readonly where?: readonly Where[];}; const recordFindMany = (adapter: DBAdapter, calls: FindManyCall[]): DBAdapter => ({  ...adapter,  findMany: (data) => {    calls.push({ model: data.model, where: data.where });    return adapter.findMany(data);  },  transaction: (callback) =>    adapter.transaction((trx) =>      callback({        ...trx,        findMany: (data) => {          calls.push({ model: data.model, where: data.where });          return trx.findMany(data);        },      }),    ),}); // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- const Item = Schema.Struct({  id: Schema.Number,  name: Schema.String,});type Item = typeof Item.Type; const EchoHeaders = Schema.Struct({  authorization: Schema.optional(Schema.String),  "x-static": Schema.optional(Schema.String),});type EchoHeaders = typeof EchoHeaders.Type; class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()(  "QueryValidationError",  {    message: Schema.String,  },) {} const ItemsGroup = HttpApiGroup.make("items")  .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) }))  .add(    HttpApiEndpoint.get("getItem", "/items/:itemId", {      params: Schema.Struct({ itemId: Schema.NumberFromString }),      success: Item,    }),  )  .add(HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders }))  .add(    HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", {      params: Schema.Struct({ entryTypeId: Schema.String }),      success: Schema.Unknown,      error: QueryValidationError,    }),  ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const spec = OpenApi.fromApi(TestApi);const specJson = JSON.stringify(spec); // ---------------------------------------------------------------------------// Implement handlers// --------------------------------------------------------------------------- const ITEMS = [  { id: 1, name: "Widget" },  { id: 2, name: "Gadget" },  { id: 3, name: "Doohickey" },]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) =>  handlers    .handle("listItems", () => Effect.succeed(ITEMS.map((item) => Item.make(item))))    .handle("getItem", (req) =>      Effect.succeed(        Item.make(          ITEMS.find((i) => i.id === req.params.itemId) ?? {            id: 0,            name: "Unknown",          },        ),      ),    )    .handle("echoHeaders", () =>      Effect.gen(function* () {        const req = yield* HttpServerRequest.HttpServerRequest;        return EchoHeaders.make({          authorization: req.headers["authorization"],          "x-static": req.headers["x-static"],        });      }),    )    .handle("queryRows", () =>      Effect.fail(        new QueryValidationError({          message: 'Field with name "DisplayName" does not exist',        }),      ),    ),); // ---------------------------------------------------------------------------// Test layer: real server on port 0 + HttpClient pointing at it// --------------------------------------------------------------------------- const ApiLive = HttpApiBuilder.layer(TestApi).pipe(Layer.provide(ItemsGroupLive)); const TestLayer = HttpRouter.serve(ApiLive, { disableListenLog: true, disableLogger: true }).pipe(  Layer.provideMerge(NodeHttpServer.layerTest),); const serveSpecRequiringHeader = () => {  const state = { requests: 0, lastToken: null as string | null };  const server = http.createServer((req, res) => {    state.requests++;    state.lastToken = req.headers["x-spec-token"]?.toString() ?? null;    if (state.lastToken !== "org-token") {      res.writeHead(401, { "content-type": "application/json" });      res.end(JSON.stringify({ error: "missing token" }));      return;    }    res.writeHead(200, { "content-type": "application/json" });    res.end(specJson);  });   return new Promise<{    readonly specUrl: string;    readonly requestCount: () => number;    readonly lastToken: () => string | null;    readonly close: () => Promise<void>;  }>((resolve) => {    server.listen(0, "127.0.0.1", () => {      const { port } = server.address() as AddressInfo;      resolve({        specUrl: `http://127.0.0.1:${port}/spec.json`,        requestCount: () => state.requests,        lastToken: () => state.lastToken,        close: () =>          new Promise((closeResolve) => {            server.close(() => closeResolve());          }),      });    });  });}; // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- layer(TestLayer)("OpenAPI Plugin", (it) => {  it.effect("previewSpec returns metadata and header presets", () =>    Effect.gen(function* () {      const server = yield* makeOpenApiTestServer({ spec });       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: server.httpClientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const preview = yield* executor.openapi.previewSpec(server.specJson);       expect(preview.operationCount).toBeGreaterThanOrEqual(2);      expect(preview.servers).toBeDefined();    }),  );   it.effect("registers static openapi executor tools", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const tools = yield* executor.tools.list();      const ids = tools.map((t) => t.id);      expect(ids).toContain("executor.openapi.previewSpec");      expect(ids).toContain("executor.openapi.addSource");    }),  );   it.effect("lists executor as the static runtime source", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const sources = yield* executor.sources.list();      expect(sources.find((s) => s.id === "openapi")).toBeUndefined();      const control = sources.find((s) => s.id === "executor");      expect(control).toBeDefined();      expect(control!.runtime).toBe(true);      expect(control!.canRemove).toBe(false);    }),  );   it.effect("invokes static previewSpec through executor.tools.invoke", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = (yield* executor.tools.invoke(        "executor.openapi.previewSpec",        { spec: specJson },        autoApprove,      )) as { operationCount: number };       expect(result.operationCount).toBeGreaterThanOrEqual(2);    }),  );   it.effect("describes static addSource parameters from Standard Schema", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          plugins: [openApiPlugin(), memorySecretsPlugin()] as const,        }),      );       const schema = yield* executor.tools.schema("executor.openapi.addSource");       expect(schema).not.toBeNull();      expect(schema!.inputTypeScript).toContain("scope: string");      expect(schema!.inputTypeScript).toContain("spec: string");      expect(        (schema!.inputSchema as { properties?: Record<string, unknown> }).properties,      ).toHaveProperty("credentialTargetScope");      expect(schema!.inputTypeScript).not.toBe("Record<string, unknown>");    }),  );   it.effect("invokes static addSource through executor.tools.invoke", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const userScope = ScopeId.make("static-user");      const orgScope = ScopeId.make("static-org");       const executor = yield* createExecutor(        makeTestConfig({          scopes: [            Scope.make({ id: userScope, name: "user", createdAt: new Date() }),            Scope.make({ id: orgScope, name: "org", createdAt: new Date() }),          ],          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       const result = (yield* executor.tools.invoke(        "executor.openapi.addSource",        { scope: String(orgScope), spec: specJson, namespace: "runtime" },        autoApprove,      )) as { sourceId: string; toolCount: number };       expect(result).toEqual({ sourceId: "runtime", toolCount: 4 });      expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull();      expect((yield* executor.openapi.getSource("runtime", String(orgScope)))?.scope).toBe(        orgScope,      );      expect((yield* executor.tools.list()).map((t) => t.id)).toContain("runtime.items.listItems");    }),  );   it.effect("requires approval before adding a source through the runtime tool", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [openApiPlugin()] as const }),      );       const declined = yield* executor.tools        .invoke(          "executor.openapi.addSource",          { scope: TEST_SCOPE, spec: specJson, namespace: "runtime_declined" },          { onElicitation: () => Effect.succeed({ action: "decline" as const }) },        )        .pipe(Effect.flip);       expect(Predicate.isTagged(declined, "ElicitationDeclinedError")).toBe(true);      expect(yield* executor.openapi.getSource("runtime_declined", TEST_SCOPE)).toBeNull();      expect((yield* executor.tools.list()).map((t) => t.id)).not.toContain(        "runtime_declined.items.listItems",      );    }),  );   it.effect("adds an org source whose direct credentials are owned by the user scope", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);      const userScope = ScopeId.make("openapi-user");      const orgScope = ScopeId.make("openapi-org");       const executor = yield* createExecutor(        makeTestConfig({          scopes: [            Scope.make({ id: userScope, name: "user", createdAt: new Date() }),            Scope.make({ id: orgScope, name: "org", createdAt: new Date() }),          ],          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        SetSecretInput.make({          id: SecretId.make("user-query-token"),          scope: userScope,          name: "User query token",          value: "user-token",        }),      );       const input = {        spec: specJson,        scope: String(orgScope),        namespace: "org_direct_user_credential",        queryParams: { token: { secretId: "user-query-token" } },        credentialTargetScope: String(userScope),      };       yield* executor.openapi.addSpec(input);       const bindings = yield* executor.openapi.listSourceBindings(        "org_direct_user_credential",        String(orgScope),      );      expect(bindings).toHaveLength(1);      expect(bindings[0]).toMatchObject({        scopeId: userScope,        slot: "query_param:token",        value: { kind: "secret", secretId: SecretId.make("user-query-token") },      });    }),  );   it.effect("updateSource removes bindings for credential slots no longer present", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        SetSecretInput.make({          id: SecretId.make("old-token"),          scope: ScopeId.make(TEST_SCOPE),          name: "Old token",          value: "old-secret",        }),      );       yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "stale_binding",        baseUrl: "",        credentialTargetScope: TEST_SCOPE,        headers: {          "X-Old": { secretId: "old-token" },        },      });       yield* executor.openapi.updateSource("stale_binding", TEST_SCOPE, {        headers: {},      });       const bindings = yield* executor.openapi.listSourceBindings("stale_binding", TEST_SCOPE);      expect(bindings).toEqual([]);    }),  );   it.effect("updateSource removes stale OAuth2 bindings when the OAuth template changes", () =>    Effect.gen(function* () {      const httpClient = yield* HttpClient.HttpClient;      const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);       const executor = yield* createExecutor(        makeTestConfig({          plugins: [            openApiPlugin({ httpClientLayer: clientLayer }),            memorySecretsPlugin(),          ] as const,        }),      );       yield* executor.secrets.set(        SetSecretInput.make({          id: SecretId.make("old-client-id"),          scope: ScopeId.make(TEST_SCOPE),          name: "Old client ID",          value: "client-id",        }),      );       const oldOAuth = OAuth2SourceConfig.make({        kind: "oauth2",        securitySchemeName: "old",        flow: "authorizationCode",        tokenUrl: "https://auth.example.com/token",        authorizationUrl: "https://auth.example.com/authorize",        clientIdSlot: "oauth2:old:client-id",        clientSecretSlot: null,        connectionSlot: "oauth2:old:connection",        scopes: ["read"],      });      yield* executor.openapi.addSpec({        spec: specJson,        scope: TEST_SCOPE,        namespace: "stale_oauth",        baseUrl: "",        oauth2: oldOAuth,      });      yield* executor.openapi.setSourceBinding(        OpenApiSourceBindingInput.make({

Lines 1–500 of 1453.