1629 lines · 52.2 KB · TypeScript
RawDownload
import { describe, it, expect } from "@effect/vitest";import { Effect, Predicate } from "effect";import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import {  ConnectionId,  CreateConnectionInput,  createExecutor,  definePlugin,  ElicitationResponse,  RemoveSecretInput,  Scope,  ScopeId,  SecretId,  TokenMaterial,} from "@executor-js/sdk";import { makeTestConfig, serveTestHttpApp } from "@executor-js/sdk/testing";import { memorySecretsPlugin } from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin";import { endpointForTelemetry } from "./invoke";import { introspect } from "./introspect";import { GRAPHQL_OAUTH_CONNECTION_SLOT, graphqlHeaderSlot, graphqlQueryParamSlot } from "./types";import type { IntrospectionResult } from "./introspect";import {  makeGreetingGraphqlSchema,  serveGraphqlFailureTestServer,  serveGraphqlTestServer,} from "../testing"; const TEST_SCOPE = "test-scope";const graphqlOAuth2Config = {  kind: "oauth2" as const,  securitySchemeName: "OAuth2",  flow: "authorizationCode" as const,  tokenUrl: "https://auth.example.test/token",  authorizationUrl: "https://auth.example.test/authorize",  clientIdSlot: "auth:oauth2:client-id",  clientSecretSlot: null,  connectionSlot: GRAPHQL_OAUTH_CONNECTION_SLOT,  scopes: [],}; // ---------------------------------------------------------------------------// Mock introspection response// --------------------------------------------------------------------------- const introspectionResult: IntrospectionResult = {  __schema: {    queryType: { name: "Query" },    mutationType: { name: "Mutation" },    types: [      {        kind: "OBJECT",        name: "Query",        description: null,        fields: [          {            name: "hello",            description: "Say hello",            args: [              {                name: "name",                description: null,                type: { kind: "SCALAR", name: "String", ofType: null },                defaultValue: null,              },            ],            type: { kind: "SCALAR", name: "String", ofType: null },          },        ],        inputFields: null,        enumValues: null,      },      {        kind: "OBJECT",        name: "Mutation",        description: null,        fields: [          {            name: "setGreeting",            description: "Set greeting message",            args: [              {                name: "message",                description: null,                type: {                  kind: "NON_NULL",                  name: null,                  ofType: { kind: "SCALAR", name: "String", ofType: null },                },                defaultValue: null,              },            ],            type: { kind: "SCALAR", name: "String", ofType: null },          },        ],        inputFields: null,        enumValues: null,      },      {        kind: "SCALAR",        name: "String",        description: null,        fields: null,        inputFields: null,        enumValues: null,      },    ],  },}; const introspectionJson = JSON.stringify({ data: introspectionResult });const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() });const declineAll = () => Effect.succeed(ElicitationResponse.make({ action: "decline" })); const sampleDataPlugin = definePlugin(() => ({  id: "sample-read-test" as const,  storage: () => ({}),  staticSources: () => [    {      id: "sample",      kind: "in-memory",      name: "Sample",      tools: [        {          name: "read",          description: "Read sample data",          inputSchema: {            type: "object",            properties: {},            additionalProperties: false,          },          handler: () => Effect.succeed("sample-value"),        },      ],    },  ],})); describe("graphqlPlugin real protocol server", () => {  it("uses query-free endpoints for invocation attributes", () => {    expect(endpointForTelemetry("https://api.example.test/graphql?token=secret#section")).toBe(      "https://api.example.test/graphql",    );  });   it.effect("includes redacted upstream text in introspection status errors", () =>    Effect.gen(function* () {      const server = yield* serveGraphqlFailureTestServer({        status: 500,        body: 'upstream failed {"access_token":"secret-value"} token=another-secret',      });       const error = yield* introspect(server.endpoint).pipe(        Effect.provide(server.httpClientLayer),        Effect.flip,      );       expect(error).toHaveProperty(        "message",        'Introspection failed with status 500: upstream failed {"access_token":"[redacted]"} token=[redacted]',      );      expect(error).not.toHaveProperty("message", expect.stringContaining("secret-value"));      expect(error).not.toHaveProperty("message", expect.stringContaining("another-secret"));    }),  );   it.effect("includes safe upstream JSON messages in introspection status errors", () =>    Effect.gen(function* () {      const server = yield* serveTestHttpApp(() =>        Effect.succeed(          HttpServerResponse.jsonUnsafe(            { message: "Resource protected by organization SSO" },            { status: 403 },          ),        ),      );       const error = yield* introspect(server.url("/graphql")).pipe(        Effect.provide(server.httpClientLayer),        Effect.flip,      );       expect(error).toHaveProperty(        "message",        "Introspection failed with status 403: Resource protected by organization SSO",      );    }),  );   it.effect("redacts secrets from upstream JSON messages in introspection status errors", () =>    Effect.gen(function* () {      const server = yield* serveTestHttpApp(() =>        Effect.succeed(          HttpServerResponse.jsonUnsafe(            { message: "Authorization: Bearer github-secret-token" },            { status: 403 },          ),        ),      );       const error = yield* introspect(server.url("/graphql")).pipe(        Effect.provide(server.httpClientLayer),        Effect.flip,      );       expect(error).toHaveProperty(        "message",        "Introspection failed with status 403: Authorization: [redacted]",      );      expect(error).not.toHaveProperty("message", expect.stringContaining("github-secret-token"));    }),  );   it.effect("accepts standard introspection responses with omitted deepest ofType", () =>    Effect.gen(function* () {      const deepType = {        kind: "NON_NULL",        name: null,        ofType: {          kind: "LIST",          name: null,          ofType: {            kind: "NON_NULL",            name: null,            ofType: {              kind: "LIST",              name: null,              ofType: {                kind: "NON_NULL",                name: null,                ofType: {                  kind: "SCALAR",                  name: "String",                },              },            },          },        },      };      const server = yield* serveTestHttpApp(() =>        Effect.succeed(          HttpServerResponse.jsonUnsafe({            data: {              __schema: {                queryType: { name: "Query" },                mutationType: null,                types: [                  {                    kind: "OBJECT",                    name: "Query",                    description: null,                    fields: [                      {                        name: "deep",                        description: null,                        args: [],                        type: deepType,                      },                    ],                    inputFields: null,                    enumValues: null,                  },                  {                    kind: "SCALAR",                    name: "String",                    description: null,                    fields: null,                    inputFields: null,                    enumValues: null,                  },                ],              },            },          }),        ),      );       const result = yield* introspect(server.url("/graphql")).pipe(        Effect.provide(server.httpClientLayer),      );       expect(result.__schema.queryType?.name).toBe("Query");    }),  );   it.effect("adds a source by introspecting the live GraphQL endpoint", () =>    Effect.gen(function* () {      const server = yield* serveGreetingServer;      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       const result = yield* executor.graphql.addSource({        endpoint: server.endpoint,        scope: TEST_SCOPE,        namespace: "live_graph",      });       expect(result).toEqual({ toolCount: 2, namespace: "live_graph" });       const tools = yield* executor.tools.list();      expect(tools.map((tool) => tool.id)).toEqual(        expect.arrayContaining(["live_graph.query.hello", "live_graph.mutation.setGreeting"]),      );       const requests = yield* server.requests;      expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true);    }),  );   it.effect("uses initial credential bindings for add-time introspection", () =>    Effect.gen(function* () {      const server = yield* serveGraphqlTestServer({        schema: makeGreetingGraphqlSchema(),        auth: {          validateAuthorization: (authorization) =>            Effect.succeed(authorization === "Bearer secret-token"),        },      });      const executor = yield* createExecutor(        makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }),      );      yield* executor.secrets.set({        id: SecretId.make("github-token"),        scope: ScopeId.make(TEST_SCOPE),        name: "GitHub token",        value: "secret-token",        provider: "memory",      });       const result = yield* executor.graphql.addSource({        endpoint: server.endpoint,        scope: TEST_SCOPE,        namespace: "initial_credentials",        headers: {          Authorization: { kind: "secret", prefix: "Bearer " },        },        credentials: {          scope: TEST_SCOPE,          headers: {            Authorization: { kind: "secret", secretId: "github-token" },          },        },      });       expect(result).toEqual({ toolCount: 2, namespace: "initial_credentials" });      const requests = yield* server.requests;      expect(        requests.some((request) => request.headers.authorization === "Bearer secret-token"),      ).toBe(true);    }),  );   it.effect(    "uses user-scoped initial credential bindings for org-scope add-time introspection",    () =>      Effect.gen(function* () {        const orgScope = "org-scope";        const userScope = "user-scope";        const server = yield* serveGraphqlTestServer({          schema: makeGreetingGraphqlSchema(),          auth: {            validateAuthorization: (authorization) =>              Effect.succeed(authorization === "Bearer user-secret-token"),          },        });        const executor = yield* createExecutor(          makeTestConfig({            scopes: [              Scope.make({                id: ScopeId.make(userScope),                name: "user",                createdAt: new Date(),              }),              Scope.make({                id: ScopeId.make(orgScope),                name: "org",                createdAt: new Date(),              }),            ],            plugins: [memorySecretsPlugin(), graphqlPlugin()] as const,          }),        );        yield* executor.secrets.set({          id: SecretId.make("github-graphql-authorization"),          scope: ScopeId.make(userScope),          name: "GitHub GraphQL Authorization",          value: "user-secret-token",          provider: "memory",        });         const result = yield* executor.graphql.addSource({          endpoint: server.endpoint,          scope: orgScope,          name: "Github GraphQL",          namespace: "github_graphql",          headers: {            Authorization: { kind: "secret", prefix: "Bearer " },          },          credentials: {            scope: userScope,            headers: {              Authorization: {                kind: "secret",                secretId: "github-graphql-authorization",                secretScope: userScope,                prefix: "Bearer ",              },            },          },        });         expect(result).toEqual({ toolCount: 2, namespace: "github_graphql" });        const requests = yield* server.requests;        expect(          requests.some((request) => request.headers.authorization === "Bearer user-secret-token"),        ).toBe(true);      }),  );   it.effect("marks source oauth-backed when add-time credentials include oauth", () =>    Effect.gen(function* () {      const server = yield* serveGraphqlTestServer({        schema: makeGreetingGraphqlSchema(),        auth: {          validateAuthorization: (authorization) =>            Effect.succeed(authorization === "Bearer oauth-token"),        },      });      const executor = yield* createExecutor(        makeTestConfig({ plugins: [memorySecretsPlugin(), graphqlPlugin()] as const }),      );      const connectionId = ConnectionId.make("graphql-oauth2-initial");      yield* executor.connections.create(        CreateConnectionInput.make({          id: connectionId,          scope: ScopeId.make(TEST_SCOPE),          provider: "oauth2",          identityLabel: "Initial GraphQL OAuth",          accessToken: TokenMaterial.make({            secretId: SecretId.make(`${connectionId}.access_token`),            name: "Initial GraphQL OAuth Access Token",            value: "oauth-token",          }),          refreshToken: null,          expiresAt: null,          oauthScope: null,          providerState: null,        }),      );       const result = yield* executor.graphql.addSource({        endpoint: server.endpoint,        scope: TEST_SCOPE,        namespace: "initial_oauth_graphql",        credentials: {          scope: TEST_SCOPE,          auth: {            oauth2: {              connection: { kind: "connection", connectionId },            },          },        },      });       expect(result).toEqual({ toolCount: 2, namespace: "initial_oauth_graphql" });      const source = yield* executor.graphql.getSource("initial_oauth_graphql", TEST_SCOPE);      expect(source?.auth.kind).toBe("oauth2");      if (source?.auth.kind !== "oauth2") return;      expect(source.auth.connectionSlot).toBe(GRAPHQL_OAUTH_CONNECTION_SLOT);    }),  );   it.effect("invokes a live query with headers and query params", () =>    Effect.gen(function* () {      const server = yield* serveGreetingServer;      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       yield* executor.graphql.addSource({        endpoint: server.endpoint,        scope: TEST_SCOPE,        namespace: "live_invoke",        headers: { "x-static": "abc" },        queryParams: { token: "qp-token" },      });      yield* server.clearRequests;       const result = yield* executor.tools.invoke("live_invoke.query.hello", {        name: "Ada",      });       expect(result).toEqual({        ok: true,        data: { hello: "Hello Ada" },      }); 

Lines 1–500 of 1629.