diff --git a/apps/web/utils/outlook/client.test.ts b/apps/web/utils/outlook/client.test.ts index 70a4151fa9..8850fff763 100644 --- a/apps/web/utils/outlook/client.test.ts +++ b/apps/web/utils/outlook/client.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { Client } from "@microsoft/microsoft-graph-client"; +import { Client, MiddlewareFactory } from "@microsoft/microsoft-graph-client"; import { saveTokens } from "@/utils/auth/save-tokens"; import { createTestLogger } from "@/__tests__/helpers"; import { @@ -16,6 +16,10 @@ import { vi.mock("@microsoft/microsoft-graph-client", () => ({ Client: { init: vi.fn(), + initWithMiddleware: vi.fn(), + }, + MiddlewareFactory: { + getDefaultMiddlewareChain: vi.fn(), }, })); @@ -58,26 +62,52 @@ vi.mock("@/env", () => ({ describe("outlook client emulator configuration", () => { beforeEach(() => { vi.clearAllMocks(); + vi.mocked(MiddlewareFactory.getDefaultMiddlewareChain).mockReturnValue([ + { + execute: vi.fn(), + setNext: vi.fn(), + }, + { + execute: vi.fn(), + setNext: vi.fn(), + }, + ]); }); - it("passes emulator-aware Graph options into the client", () => { + it("authenticates requests to the HTTP emulator", async () => { createOutlookClient("emulator-token", createTestLogger()); expect(getMicrosoftGraphClientOptions).toHaveBeenCalledWith( "emulator-token", ); - expect(Client.init).toHaveBeenCalledWith({ - authProvider: expect.any(Function), + expect(Client.initWithMiddleware).toHaveBeenCalledWith({ baseUrl: "http://localhost:4003/", customHosts: new Set(["localhost"]), defaultVersion: "v1.0", fetchOptions: { headers: { - Authorization: "Bearer emulator-token", Prefer: 'IdType="ImmutableId"', }, }, + middleware: [expect.any(Object), expect.any(Object)], }); + + const options = vi.mocked(Client.initWithMiddleware).mock.calls[0]?.[0]; + const middleware = Array.isArray(options?.middleware) + ? options.middleware + : []; + const request = { + request: "http://localhost:4003/v1.0/me", + options: { headers: {} }, + }; + + middleware[0]?.setNext?.(middleware[1]!); + await middleware[0]?.execute(request); + + expect(new Headers(request.options.headers).get("Authorization")).toBe( + "Bearer emulator-token", + ); + expect(middleware[1]?.execute).toHaveBeenCalledWith(request); }); it("uses the emulator authorize URL for linking", () => { diff --git a/apps/web/utils/outlook/client.ts b/apps/web/utils/outlook/client.ts index ba7c6bcfea..1b18f82ebf 100644 --- a/apps/web/utils/outlook/client.ts +++ b/apps/web/utils/outlook/client.ts @@ -1,4 +1,4 @@ -import { Client } from "@microsoft/microsoft-graph-client"; +import { Client, MiddlewareFactory } from "@microsoft/microsoft-graph-client"; import type { User } from "@microsoft/microsoft-graph-types"; import { saveTokens } from "@/utils/auth/save-tokens"; import { cleanupInvalidTokens } from "@/utils/auth/cleanup-invalid-tokens"; @@ -27,6 +27,29 @@ export class OutlookClient { this.accessToken = accessToken; this.logger = logger; const graphClientOptions = getMicrosoftGraphClientOptions(accessToken); + const fetchOptions = { + headers: { + Prefer: 'IdType="ImmutableId"', + }, + }; + const emulatorAuthorization = + graphClientOptions.fetchOptions?.headers?.Authorization; + + if (emulatorAuthorization) { + const middleware = MiddlewareFactory.getDefaultMiddlewareChain({ + getAccessToken: async () => this.accessToken, + }); + middleware[0] = createEmulatorAuthenticationMiddleware( + emulatorAuthorization, + ); + this.client = Client.initWithMiddleware({ + ...graphClientOptions, + fetchOptions, + middleware, + }); + return; + } + this.client = Client.init({ authProvider: (done) => { done(null, this.accessToken); @@ -35,12 +58,7 @@ export class OutlookClient { ...graphClientOptions, // Use immutable IDs to ensure message IDs remain stable // https://learn.microsoft.com/en-us/graph/outlook-immutable-id - fetchOptions: { - headers: { - ...(graphClientOptions.fetchOptions?.headers ?? {}), - Prefer: 'IdType="ImmutableId"', - }, - }, + fetchOptions, }); } @@ -256,5 +274,33 @@ export function getLinkingOAuth2Url() { return `${getMicrosoftOauthAuthorizeUrl()}?${params.toString()}`; } +type GraphMiddleware = ReturnType< + typeof MiddlewareFactory.getDefaultMiddlewareChain +>[number]; + +function createEmulatorAuthenticationMiddleware( + authorization: string, +): GraphMiddleware { + let nextMiddleware: GraphMiddleware | undefined; + + return { + async execute(context) { + context.options ??= {}; + const headers = new Headers(context.options.headers); + headers.set("Authorization", authorization); + context.options.headers = headers; + + if (!nextMiddleware) { + throw new Error("Outlook emulator middleware is not configured"); + } + + await nextMiddleware.execute(context); + }, + setNext(middleware) { + nextMiddleware = middleware; + }, + }; +} + // Helper types for common Microsoft Graph operations export type { Client as GraphClient };