import { randomBytes } from "node:crypto"; import { createServer } from "node:net"; import { expect } from "@effect/vitest"; import { Effect } from "effect"; import { composePluginApi } from "@executor-js/api/server"; import { createEmulator, type Emulator, type IssuedCredential } from "@executor-js/emulate"; import { MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, microsoftCatalog, microsoftGraphAdapter, } from "@executor-js/plugin-openapi/providers/microsoft"; import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, OAuthClientSlug, } from "@executor-js/sdk/shared"; import { scenario } from "../src/scenario"; import { Api, Target } from "../src/services"; const api = composePluginApi([ openApiHttpPlugin({ presets: microsoftCatalog, specFormats: [microsoftGraphAdapter] }), ] as const); type OAuthTemplateView = { readonly slug: string; readonly kind: string; readonly scopes?: readonly string[]; }; type ToolView = { readonly name: string; }; const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; const availablePort = Effect.callback((resume) => { const server = createServer(); server.listen(0, "127.0.0.1", () => { const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; server.close(() => { resume(Effect.succeed(port)); }); }); }); const microsoftEmulator = Effect.acquireRelease( Effect.gen(function* () { const port = yield* availablePort; return yield* Effect.promise(() => createEmulator({ service: "microsoft", port })); }), (emulator: Emulator) => Effect.promise(() => emulator.close()).pipe(Effect.ignore), ); const requireOAuthClientCredential = (credential: IssuedCredential) => Effect.gen(function* () { if ( credential.client_id && credential.client_secret && credential.authorization_url && credential.token_url ) { return { clientId: credential.client_id, clientSecret: credential.client_secret, authorizationUrl: credential.authorization_url, tokenUrl: credential.token_url, }; } return yield* Effect.die("Microsoft emulator returned incomplete OAuth client credentials."); }); const listUsersCode = (integration: string) => ` const found = await tools.search({ namespace: ${JSON.stringify(integration)}, query: "list users", limit: 10 }); const item = found.items.find((candidate) => candidate.path.includes("graphUser")); if (!item) return { ok: false, error: "graph users tool not found", found }; let callable = tools; for (const segment of item.path.split(".")) callable = callable[segment]; const result = await callable({}); return { ok: result.ok, path: item.path, result: result.ok ? result.data : result.error }; `; scenario( "Microsoft ยท client credentials against the emulator mint a Graph connection and call /users", { timeout: 180_000 }, Effect.scoped( Effect.gen(function* () { const target = yield* Target; const { client: makeApiClient } = yield* Api; const identity = yield* target.newIdentity(); const client = yield* makeApiClient(api, identity); const emulator = yield* microsoftEmulator; const integration = unique("msgraph"); const oauthClient = OAuthClientSlug.make(unique("msgraph_app")); const connection = ConnectionName.make("machine"); const credential = yield* Effect.promise(() => emulator.credentials.mint({ type: "oauth-client-credentials", name: "Executor E2E Graph" }), ); const oauth = yield* requireOAuthClientCredential(credential); yield* Effect.ensuring( Effect.gen(function* () { yield* client.openapi.addSpec({ payload: { spec: { kind: "url", url: emulator.openapiUrl }, slug: integration, name: "Microsoft Graph Emulator", baseUrl: emulator.url, family: "microsoft", authenticationTemplate: [ { slug: MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, kind: "oauth2", authorizationUrl: oauth.authorizationUrl, tokenUrl: oauth.tokenUrl, scopes: ["https://graph.microsoft.com/.default"], }, ], }, }); const config = yield* client.openapi.getConfig({ params: { slug: integration }, }); const appOnlyTemplate = config?.authenticationTemplate?.find( (template: OAuthTemplateView) => template.slug === MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, ); expect( appOnlyTemplate?.kind, "the Microsoft integration exposes an app-only OAuth method", ).toBe("oauth2"); expect( appOnlyTemplate?.kind === "oauth2" ? appOnlyTemplate.scopes : [], "client credentials use the real Microsoft Graph .default scope", ).toEqual(["https://graph.microsoft.com/.default"]); yield* client.oauth.createClient({ payload: { owner: "org", slug: oauthClient, authorizationUrl: oauth.authorizationUrl, tokenUrl: oauth.tokenUrl, grant: "client_credentials", clientId: oauth.clientId, clientSecret: oauth.clientSecret, }, }); const started = yield* client.oauth.start({ payload: { client: oauthClient, clientOwner: "org", owner: "org", name: connection, integration: IntegrationSlug.make(integration), template: AuthTemplateSlug.make(MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG), }, }); expect(started.status, "client credentials OAuth completes without browser consent").toBe( "connected", ); const tools = yield* client.tools.list({ query: { integration: IntegrationSlug.make(integration), connection }, }); expect( tools.map((tool: ToolView) => tool.name), "the Graph /users operation is available for the app-only connection", ).toContain("users.graphUserList"); const executed = yield* client.executions.execute({ payload: { code: listUsersCode(integration) }, }); expect(executed.status, "the Graph tool execution completed").toBe("completed"); if (executed.status !== "completed") return; expect(executed.isError, executed.text).toBe(false); const body = JSON.parse(executed.text) as { readonly ok?: boolean; readonly result?: { readonly value?: readonly unknown[] }; }; expect(body.ok, executed.text).toBe(true); expect(Array.isArray(body.result?.value), executed.text).toBe(true); const ledger = yield* Effect.promise(() => emulator.ledger.list()); const tokenRequest = ledger.find((entry) => entry.path === "/oauth2/v2.0/token"); expect(tokenRequest?.response.status, "Executor exchanged client credentials").toBe(200); expect( tokenRequest?.request.body, "the token exchange requested Microsoft Graph .default", ).toMatchObject({ grant_type: "client_credentials", scope: "https://graph.microsoft.com/.default", }); const usersRequest = ledger.find((entry) => entry.operationId === "graphUser_List"); expect( usersRequest?.response.status, "Executor called the emulator Graph /users API", ).toBe(200); }), Effect.gen(function* () { yield* client.connections.remove({ params: { owner: "org", integration: IntegrationSlug.make(integration), name: connection, }, }); yield* client.oauth.removeClient({ params: { slug: oauthClient }, payload: { owner: "org" }, }); yield* client.openapi.removeSpec({ params: { slug: integration } }); }).pipe(Effect.ignore), ); }), ), );