896 lines · 29.6 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Layer } from "effect";import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; import {  AuthTemplateSlug,  ConnectionName,  IntegrationSlug,  createExecutor,} from "@executor-js/sdk";import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; import { microsoftPlugin, type MicrosoftPluginOptions } from "./plugin";import {  MICROSOFT_AUTH_TEMPLATE_SLUG,  MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG,  MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES,  MICROSOFT_GRAPH_OPENAPI_URL,  MICROSOFT_GRAPH_PERMISSIONS_REFERENCE_URL,} from "./presets"; const graphFixture = `openapi: 3.0.4info:  title: Microsoft Graph Fixture  version: v1.0servers:  - url: https://graph.microsoft.com/v1.0paths:  /me:    get:      operationId: me.GetUser      summary: Get the signed-in user profile      security:        - azureAdDelegated:            - User.Read      responses:        "200":          description: OK  /me/messages:    get:      operationId: me.messages.ListMessages      security:        - azureAdDelegated:            - Mail.ReadWrite      responses:        "200":          description: OK  /me/events:    get:      operationId: me.events.ListEvents      security:        - azureAdDelegated:            - Calendars.ReadWrite      responses:        "200":          description: OK  /me/onenote/pages:    get:      operationId: me.onenote.pages.ListPages      security:        - azureAdDelegated:            - Notes.ReadWrite      responses:        "200":          description: OK  /me/drive/root/children:    get:      operationId: me.drive.root.ListChildren      responses:        "200":          description: OK  /sites:    get:      operationId: sites.ListSites      responses:        "200":          description: OKcomponents:  schemas:    user:      type: object`; const permissionsReferenceFixture = `### User.Read | Category | Application | Delegated ||--|--|--|| Identifier | - | e1fe6dd8-ba31-4d61-89e7-88639da4683d | --- ### Mail.ReadWrite | Category | Application | Delegated ||--|--|--|| Identifier | e2a3a72e-5f79-4c64-b1b1-878b674786c9 | 024d486e-b451-40bb-833d-3e66d98c5c73 | --- ### Calendars.ReadWrite | Category | Application | Delegated ||--|--|--|| Identifier | ef54d2bf-783f-4e0f-bca1-3210c0444d99 | 1ec239c2-d7c9-4623-a91a-a9775856bb36 | --- ### Notes.ReadWrite | Category | Application | Delegated ||--|--|--|| Identifier | 085ca537-6565-41c2-aca7-db852babc212 | 615e82d5-1f7f-4f99-a456-0a0484a820d5 | --- ### AppCatalog.Read.All | Category | Application | Delegated ||--|--|--|| Identifier | e12dae10-5a57-4817-b79d-dfbec5348930 | - |`; const EMULATOR_SPEC_URL = "https://microsoft.emulators.dev/_emulate/openapi";const EMULATOR_BASE_URL = "https://microsoft.emulators.dev";const LOCAL_EMULATOR_SPEC_URL = "http://localhost:4123/_emulate/openapi";const LOCAL_EMULATOR_BASE_URL = "http://localhost:4123";const emulatorGraphFixture = `openapi: 3.0.3info:  title: Microsoft Graph Emulator  version: 1.0.0servers:  - url: ${EMULATOR_BASE_URL}paths:  /v1.0/users:    get:      operationId: graphUser_List      security:        - azureAdDelegated:            - User.Read.All      responses:        "200":          description: OK  /v1.0/me:    get:      operationId: graphUser_GetMyProfile      responses:        "200":          description: OKcomponents:  securitySchemes:    azureAdDelegated:      type: oauth2      flows:        authorizationCode:          authorizationUrl: ${EMULATOR_BASE_URL}/oauth2/v2.0/authorize          tokenUrl: ${EMULATOR_BASE_URL}/oauth2/v2.0/token          scopes:            User.Read: User.Read            User.Read.All: User.Read.All        clientCredentials:          tokenUrl: ${EMULATOR_BASE_URL}/oauth2/v2.0/token          scopes:            https://graph.microsoft.com/.default: https://graph.microsoft.com/.default`; const localEmulatorGraphFixture = `openapi: 3.0.3info:  title: Microsoft Graph Local Emulator  version: 1.0.0servers:  - url: ${LOCAL_EMULATOR_BASE_URL}paths:  /v1.0/users:    get:      operationId: graphUser_List      security:        - azureAdDelegated:            - User.Read.All      responses:        "200":          description: OKcomponents:  securitySchemes:    azureAdDelegated:      type: oauth2      flows:        clientCredentials:          tokenUrl: ${LOCAL_EMULATOR_BASE_URL}/oauth2/v2.0/token          scopes:            https://graph.microsoft.com/.default: https://graph.microsoft.com/.default`; interface GraphHttpClientOptions {  readonly failedUrls?: ReadonlySet<string>;  readonly beforeResponse?: (url: string) => Effect.Effect<void, never, never>;} const waitOneTurn: Effect.Effect<void, never, never> = Effect.promise(() => Promise.resolve()).pipe(  Effect.orDie,); const GRAPH_FIXTURE_BODIES: Readonly<Record<string, string>> = {  [MICROSOFT_GRAPH_OPENAPI_URL]: graphFixture,  [MICROSOFT_GRAPH_PERMISSIONS_REFERENCE_URL]: permissionsReferenceFixture,  [EMULATOR_SPEC_URL]: emulatorGraphFixture,  [LOCAL_EMULATOR_SPEC_URL]: localEmulatorGraphFixture,}; const makeGraphHttpClientLayer = (options: GraphHttpClientOptions = {}) =>  Layer.succeed(HttpClient.HttpClient)(    HttpClient.make((request: HttpClientRequest.HttpClientRequest) => {      const body = GRAPH_FIXTURE_BODIES[request.url];      const failed = options.failedUrls?.has(request.url) === true;      const response = HttpClientResponse.fromWeb(        request,        failed          ? new Response("forced failure", { status: 503 })          : body === undefined            ? new Response("not found", { status: 404 })            : new Response(body, {                status: 200,                headers: {                  "content-type":                    request.url === MICROSOFT_GRAPH_PERMISSIONS_REFERENCE_URL                      ? "text/markdown"                      : "application/yaml",                },              }),      );      return (options.beforeResponse?.(request.url) ?? Effect.void).pipe(Effect.as(response));    }),  ); const graphHttpClientLayer = makeGraphHttpClientLayer(); const graphPlugins = (options?: Omit<MicrosoftPluginOptions, "httpClientLayer">) =>  [    microsoftPlugin({ httpClientLayer: graphHttpClientLayer, ...options }),    memoryCredentialsPlugin(),  ] as const; describe("Microsoft Graph provider", () => {  it.effect("rejects non-Microsoft URL overrides before fetching the Graph spec", () =>    Effect.scoped(      Effect.gen(function* () {        let requests = 0;        const blockedHttpClientLayer = Layer.succeed(HttpClient.HttpClient)(          HttpClient.make((request: HttpClientRequest.HttpClientRequest) =>            Effect.sync(() => {              requests += 1;              return HttpClientResponse.fromWeb(                request,                new Response("unexpected request", { status: 500 }),              );            }),          ),        );        const executor = yield* createExecutor(          makeTestConfig({            plugins: [              microsoftPlugin({ httpClientLayer: blockedHttpClientLayer }),              memoryCredentialsPlugin(),            ],          }),        );         const result = yield* executor.microsoft.addWorkloads({          baseUrl: "https://attacker.example/v1.0",          workloads: [            {              custom: {                customScopes: ["User.Read.All"],                slug: "bad_graph",                name: "Bad Graph",                specUrl: "https://attacker.example/openapi.yaml",                authorizationUrl: "https://attacker.example/oauth2/v2.0/authorize",                tokenUrl: "https://attacker.example/oauth2/v2.0/token",                clientCredentialsTokenUrl: "https://attacker.example/oauth2/v2.0/token",              },            },          ],        });         expect(result).toEqual({          added: [],          skipped: [],          failed: [            {              slug: IntegrationSlug.make("bad_graph"),              presetId: "custom",              error:                "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI source",            },          ],        });        expect(requests).toBe(0);      }),    ),  );   it.effect("custom workload with custom scopes stays custom-only, no default workloads", () =>    Effect.scoped(      Effect.gen(function* () {        const executor = yield* createExecutor(makeTestConfig({ plugins: graphPlugins() }));         const result = yield* executor.microsoft.addWorkloads({          workloads: [            {              custom: {                customScopes: ["Custom.Scope"],                slug: "microsoft_graph_custom",                name: "Custom Microsoft Graph",              },            },          ],        });         expect(result).toEqual({          added: [            {              slug: IntegrationSlug.make("microsoft_graph_custom"),              presetId: "custom",              toolCount: 1,            },          ],          skipped: [],          failed: [],        });         const config = yield* executor.microsoft.getConfig("microsoft_graph_custom");        expect(config?.microsoftGraphPresetIds).toEqual([]);        expect(config?.microsoftGraphCustomScopes).toEqual(["Custom.Scope"]);        expect(config?.microsoftGraphCoversFullGraph).toBe(false);        expect(config?.microsoftGraphScopes).toEqual([          "offline_access",          "User.Read",          "Custom.Scope",        ]);      }),    ),  );   it.effect(    "adds a custom Microsoft workload from the emulator spec with app-only OAuth endpoints",    () =>      Effect.scoped(        Effect.gen(function* () {          const executor = yield* createExecutor(            makeTestConfig({ plugins: graphPlugins({ allowUnsafeUrlOverrides: true }) }),          );           yield* executor.microsoft.addWorkloads({            baseUrl: EMULATOR_BASE_URL,            workloads: [              {                custom: {                  customScopes: ["User.Read.All"],                  slug: "microsoft_graph_emulated",                  name: "Microsoft Graph Emulator",                  specUrl: EMULATOR_SPEC_URL,                },              },            ],          });           const config = yield* executor.microsoft.getConfig("microsoft_graph_emulated");          expect(config?.sourceUrl).toBe(EMULATOR_SPEC_URL);          expect(config?.baseUrl).toBe(EMULATOR_BASE_URL);          expect(config?.microsoftGraphAuthorizationUrl).toBe(            `${EMULATOR_BASE_URL}/oauth2/v2.0/authorize`,          );          expect(config?.microsoftGraphTokenUrl).toBe(`${EMULATOR_BASE_URL}/oauth2/v2.0/token`);          expect(config?.microsoftGraphClientCredentialsTokenUrl).toBe(            `${EMULATOR_BASE_URL}/oauth2/v2.0/token`,          );           const delegated = config?.authenticationTemplate?.find(            (entry) =>              entry.kind === "oauth2" && String(entry.slug) === MICROSOFT_AUTH_TEMPLATE_SLUG,          );          const clientCredentials = config?.authenticationTemplate?.find(            (entry) =>              entry.kind === "oauth2" &&              String(entry.slug) === MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG,          );          expect(delegated?.kind === "oauth2" ? delegated.tokenUrl : undefined).toBe(            `${EMULATOR_BASE_URL}/oauth2/v2.0/token`,          );          expect(            clientCredentials?.kind === "oauth2" ? clientCredentials.scopes : undefined,          ).toEqual([...MICROSOFT_GRAPH_CLIENT_CREDENTIALS_SCOPES]);           yield* executor.connections.create({            owner: "org",            name: ConnectionName.make("machine"),            integration: IntegrationSlug.make("microsoft_graph_emulated"),            template: AuthTemplateSlug.make(MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG),            value: "token-xyz",          });           const toolNames = (yield* executor.tools.list()).map((tool) => String(tool.name));          expect(toolNames).toContain("users.graphUserList");        }),      ),  );   it.effect("accepts a loopback http emulator spec only when the override is enabled", () =>    Effect.scoped(      Effect.gen(function* () {        const executor = yield* createExecutor(          makeTestConfig({ plugins: graphPlugins({ allowUnsafeUrlOverrides: true }) }),        );         yield* executor.microsoft.addWorkloads({          baseUrl: LOCAL_EMULATOR_BASE_URL,          workloads: [            {              custom: {                customScopes: ["User.Read.All"],                slug: "microsoft_graph_local_emulated",                name: "Microsoft Graph Local Emulator",                specUrl: LOCAL_EMULATOR_SPEC_URL,              },            },          ],        });         const config = yield* executor.microsoft.getConfig("microsoft_graph_local_emulated");        expect(config?.sourceUrl).toBe(LOCAL_EMULATOR_SPEC_URL);        expect(config?.baseUrl).toBe(LOCAL_EMULATOR_BASE_URL);      }),    ),  );   it.effect("rejects a loopback http spec URL when the override is disabled", () =>    Effect.scoped(      Effect.gen(function* () {        const executor = yield* createExecutor(makeTestConfig({ plugins: graphPlugins() }));         const result = yield* executor.microsoft.addWorkloads({          baseUrl: LOCAL_EMULATOR_BASE_URL,          workloads: [            {              custom: {                customScopes: ["User.Read.All"],                slug: "microsoft_graph_local_disabled",                name: "Microsoft Graph Local Emulator",                specUrl: LOCAL_EMULATOR_SPEC_URL,              },            },          ],        });         expect(result.failed).toEqual([          {            slug: IntegrationSlug.make("microsoft_graph_local_disabled"),            presetId: "custom",            error:              "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI source",          },        ]);      }),    ),  );   it.effect("rejects a non-loopback http override even with allowUnsafeUrlOverrides", () =>    Effect.scoped(      Effect.gen(function* () {        let requests = 0;        const blockedHttpClientLayer = Layer.succeed(HttpClient.HttpClient)(          HttpClient.make((request: HttpClientRequest.HttpClientRequest) =>            Effect.sync(() => {              requests += 1;              return HttpClientResponse.fromWeb(                request,                new Response("unexpected request", { status: 500 }),              );            }),          ),        );        const executor = yield* createExecutor(          makeTestConfig({            plugins: [              microsoftPlugin({                httpClientLayer: blockedHttpClientLayer,                allowUnsafeUrlOverrides: true,              }),              memoryCredentialsPlugin(),            ],          }),        );         const result = yield* executor.microsoft.addWorkloads({          baseUrl: "http://example.com/v1.0",          workloads: [            {

Lines 1–500 of 896.