Skip to content

Commit 0d498d4

Browse files
Add PCAFactory and correlation id optional params to createNestablePublicClientApplication
1 parent 6a444bc commit 0d498d4

File tree

2 files changed

+273
-5
lines changed

2 files changed

+273
-5
lines changed

lib/msal-browser/src/app/PublicClientApplication.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -373,23 +373,27 @@ export class PublicClientApplication implements IPublicClientApplication {
373373
* falls back to StandardController if NestedAppAuthController is not available
374374
*
375375
* @param configuration
376+
* @param correlationId
377+
* @param pcaFactory
376378
* @returns IPublicClientApplication
377379
*
378380
*/
379381
export async function createNestablePublicClientApplication(
380-
configuration: Configuration
382+
configuration: Configuration,
383+
correlationId?: string,
384+
pcaFactory?: (configuration: Configuration, controller: IController) => IPublicClientApplication,
381385
): Promise<IPublicClientApplication> {
382-
const correlationId = createNewGuid();
386+
const cid = correlationId || createNewGuid();
383387
const nestedAppAuth = new NestedAppOperatingContext(configuration);
384-
await nestedAppAuth.initialize(correlationId);
388+
await nestedAppAuth.initialize(cid);
385389

386390
if (nestedAppAuth.isAvailable()) {
387391
const controller = new NestedAppAuthController(nestedAppAuth);
388-
const nestablePCA = new PublicClientApplication(
392+
const nestablePCA = pcaFactory ? pcaFactory(configuration, controller) : new PublicClientApplication(
389393
configuration,
390394
controller
391395
);
392-
await nestablePCA.initialize({ correlationId });
396+
await nestablePCA.initialize({ correlationId: cid });
393397
return nestablePCA;
394398
}
395399

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import {
7+
PublicClientApplication,
8+
createNestablePublicClientApplication,
9+
createStandardPublicClientApplication,
10+
} from "../../src/app/PublicClientApplication.js";
11+
import {
12+
TEST_CONFIG,
13+
RANDOM_TEST_GUID,
14+
} from "../utils/StringConstants.js";
15+
import { NestedAppOperatingContext } from "../../src/operatingcontext/NestedAppOperatingContext.js";
16+
import { NestedAppAuthController } from "../../src/controllers/NestedAppAuthController.js";
17+
import { Configuration, IPublicClientApplication } from "../../src/index.js";
18+
import { IController } from "../../src/controllers/IController.js";
19+
import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js";
20+
21+
// Mock modules
22+
jest.mock("../../src/operatingcontext/NestedAppOperatingContext.js");
23+
jest.mock("../../src/controllers/NestedAppAuthController.js");
24+
25+
// Mock createStandardPublicClientApplication function
26+
jest.mock("../../src/app/PublicClientApplication.js", () => ({
27+
...jest.requireActual("../../src/app/PublicClientApplication.js"),
28+
createStandardPublicClientApplication: jest.fn(),
29+
}));
30+
31+
describe("createNestablePublicClientApplication tests", () => {
32+
let mockNestedAppOperatingContext: jest.Mocked<NestedAppOperatingContext>;
33+
let mockNestedAppAuthController: jest.Mocked<NestedAppAuthController>;
34+
let testConfig: Configuration;
35+
let createNewGuidSpy: jest.SpyInstance;
36+
let createStandardSpy: jest.SpyInstance;
37+
38+
beforeEach(() => {
39+
testConfig = {
40+
auth: {
41+
clientId: TEST_CONFIG.MSAL_CLIENT_ID,
42+
authority: TEST_CONFIG.validAuthority,
43+
},
44+
};
45+
46+
// Create fresh mock instances for each test
47+
mockNestedAppOperatingContext = {
48+
initialize: jest.fn().mockResolvedValue(true),
49+
isAvailable: jest.fn(),
50+
} as any;
51+
52+
mockNestedAppAuthController = {} as any;
53+
54+
// Mock constructors
55+
(NestedAppOperatingContext as jest.MockedClass<typeof NestedAppOperatingContext>).mockImplementation(() => mockNestedAppOperatingContext);
56+
(NestedAppAuthController as jest.MockedClass<typeof NestedAppAuthController>).mockImplementation(() => mockNestedAppAuthController);
57+
58+
// Mock PublicClientApplication
59+
jest.spyOn(PublicClientApplication.prototype, "initialize").mockResolvedValue(undefined);
60+
61+
// Mock createNewGuid
62+
createNewGuidSpy = jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue(RANDOM_TEST_GUID);
63+
64+
// Get the mocked function
65+
createStandardSpy = createStandardPublicClientApplication as jest.MockedFunction<typeof createStandardPublicClientApplication>;
66+
});
67+
68+
afterEach(() => {
69+
jest.restoreAllMocks();
70+
});
71+
72+
describe("When nestedAppAuth.isAvailable() returns true", () => {
73+
beforeEach(() => {
74+
mockNestedAppOperatingContext.isAvailable.mockReturnValue(true);
75+
});
76+
77+
describe("Without pcaFactory parameter", () => {
78+
it("should create a nestable PCA using default PublicClientApplication constructor", async () => {
79+
const result = await createNestablePublicClientApplication(testConfig);
80+
81+
// Verify NestedAppOperatingContext was created with config
82+
expect(NestedAppOperatingContext).toHaveBeenCalledWith(testConfig);
83+
84+
// Verify initialize was called with generated correlation ID
85+
expect(mockNestedAppOperatingContext.initialize).toHaveBeenCalledWith(RANDOM_TEST_GUID);
86+
87+
// Verify isAvailable was called
88+
expect(mockNestedAppOperatingContext.isAvailable).toHaveBeenCalled();
89+
90+
// Verify NestedAppAuthController was created with the operating context
91+
expect(NestedAppAuthController).toHaveBeenCalledWith(mockNestedAppOperatingContext);
92+
93+
// Verify the result is a PublicClientApplication instance
94+
expect(result).toBeInstanceOf(PublicClientApplication);
95+
96+
// Verify initialize was called on the PCA instance
97+
expect(result.initialize).toHaveBeenCalledWith({ correlationId: RANDOM_TEST_GUID });
98+
});
99+
100+
it("should use provided correlation ID when specified", async () => {
101+
// Create a fresh spy for this test to avoid interference
102+
const freshGuidSpy = jest.spyOn(BrowserCrypto, "createNewGuid");
103+
const customCorrelationId = "custom-correlation-id";
104+
105+
const result = await createNestablePublicClientApplication(testConfig, customCorrelationId);
106+
107+
// Verify initialize was called with custom correlation ID
108+
expect(mockNestedAppOperatingContext.initialize).toHaveBeenCalledWith(customCorrelationId);
109+
110+
// Verify initialize was called on PCA with custom correlation ID
111+
expect(result.initialize).toHaveBeenCalledWith({ correlationId: customCorrelationId });
112+
113+
// Verify createNewGuid was not called since correlation ID was provided
114+
expect(freshGuidSpy).not.toHaveBeenCalled();
115+
116+
freshGuidSpy.mockRestore();
117+
});
118+
119+
it("should generate correlation ID when not provided", async () => {
120+
await createNestablePublicClientApplication(testConfig);
121+
122+
// Verify createNewGuid was called
123+
expect(createNewGuidSpy).toHaveBeenCalled();
124+
125+
// Verify initialize was called with generated ID
126+
expect(mockNestedAppOperatingContext.initialize).toHaveBeenCalledWith(RANDOM_TEST_GUID);
127+
});
128+
});
129+
130+
describe("With pcaFactory parameter", () => {
131+
it("should use pcaFactory to create the PCA when provided", async () => {
132+
const mockPCA = {
133+
initialize: jest.fn().mockResolvedValue(undefined),
134+
} as any;
135+
136+
const mockPcaFactory = jest.fn().mockReturnValue(mockPCA);
137+
138+
const result = await createNestablePublicClientApplication(
139+
testConfig,
140+
undefined,
141+
mockPcaFactory
142+
);
143+
144+
// Verify pcaFactory was called with config and controller
145+
expect(mockPcaFactory).toHaveBeenCalledWith(testConfig, mockNestedAppAuthController);
146+
147+
// Verify the returned PCA is the one from factory
148+
expect(result).toBe(mockPCA);
149+
150+
// Verify initialize was called on the factory-created PCA
151+
expect(mockPCA.initialize).toHaveBeenCalledWith({ correlationId: RANDOM_TEST_GUID });
152+
});
153+
154+
it("should use pcaFactory with custom correlation ID", async () => {
155+
// Create a fresh spy for this test to avoid interference
156+
const freshGuidSpy = jest.spyOn(BrowserCrypto, "createNewGuid");
157+
const customCorrelationId = "factory-correlation-id";
158+
const mockPCA = {
159+
initialize: jest.fn().mockResolvedValue(undefined),
160+
} as any;
161+
162+
const mockPcaFactory = jest.fn().mockReturnValue(mockPCA);
163+
164+
const result = await createNestablePublicClientApplication(
165+
testConfig,
166+
customCorrelationId,
167+
mockPcaFactory
168+
);
169+
170+
// Verify pcaFactory was called with config and controller
171+
expect(mockPcaFactory).toHaveBeenCalledWith(testConfig, mockNestedAppAuthController);
172+
173+
// Verify initialize was called with custom correlation ID
174+
expect(mockPCA.initialize).toHaveBeenCalledWith({ correlationId: customCorrelationId });
175+
176+
// Verify createNewGuid was not called
177+
expect(freshGuidSpy).not.toHaveBeenCalled();
178+
179+
freshGuidSpy.mockRestore();
180+
});
181+
182+
it("should handle pcaFactory returning different PCA implementation", async () => {
183+
class CustomPCA implements IPublicClientApplication {
184+
constructor(public config: Configuration, public controller: IController) {}
185+
initialize = jest.fn().mockResolvedValue(undefined);
186+
// Add other required methods with minimal implementations
187+
acquireTokenPopup = jest.fn();
188+
acquireTokenRedirect = jest.fn();
189+
acquireTokenSilent = jest.fn();
190+
acquireTokenByCode = jest.fn();
191+
addEventCallback = jest.fn();
192+
removeEventCallback = jest.fn();
193+
addPerformanceCallback = jest.fn();
194+
removePerformanceCallback = jest.fn();
195+
getAccount = jest.fn();
196+
getAllAccounts = jest.fn();
197+
handleRedirectPromise = jest.fn();
198+
loginPopup = jest.fn();
199+
loginRedirect = jest.fn();
200+
logoutRedirect = jest.fn();
201+
logoutPopup = jest.fn();
202+
ssoSilent = jest.fn();
203+
getLogger = jest.fn();
204+
setLogger = jest.fn();
205+
setActiveAccount = jest.fn();
206+
getActiveAccount = jest.fn();
207+
initializeWrapperLibrary = jest.fn();
208+
setNavigationClient = jest.fn();
209+
getConfiguration = jest.fn();
210+
hydrateCache = jest.fn();
211+
clearCache = jest.fn();
212+
}
213+
214+
const mockPcaFactory = jest.fn().mockImplementation((config, controller) => {
215+
return new CustomPCA(config, controller);
216+
});
217+
218+
const result = await createNestablePublicClientApplication(
219+
testConfig,
220+
undefined,
221+
mockPcaFactory
222+
);
223+
224+
// Verify result is instance of CustomPCA
225+
expect(result).toBeInstanceOf(CustomPCA);
226+
expect(result.initialize).toHaveBeenCalledWith({ correlationId: RANDOM_TEST_GUID });
227+
});
228+
});
229+
});
230+
231+
describe("When nestedAppAuth.isAvailable() returns false", () => {
232+
beforeEach(() => {
233+
mockNestedAppOperatingContext.isAvailable.mockReturnValue(false);
234+
});
235+
236+
it("should call createStandardPublicClientApplication as fallback", async () => {
237+
const mockStandardPCA = { type: "standard" } as any;
238+
createStandardSpy.mockResolvedValue(mockStandardPCA);
239+
240+
const result = await createNestablePublicClientApplication(testConfig);
241+
242+
// Should have initialized NestedAppOperatingContext first
243+
expect(mockNestedAppOperatingContext.initialize).toHaveBeenCalledWith(RANDOM_TEST_GUID);
244+
expect(mockNestedAppOperatingContext.isAvailable).toHaveBeenCalled();
245+
246+
// Should return a result (whether mocked or real)
247+
expect(result).toBeDefined();
248+
});
249+
250+
it("should ignore pcaFactory parameter when falling back", async () => {
251+
const mockPcaFactory = jest.fn();
252+
253+
const result = await createNestablePublicClientApplication(
254+
testConfig,
255+
undefined,
256+
mockPcaFactory
257+
);
258+
259+
// The factory should not be called since we're falling back
260+
expect(mockPcaFactory).not.toHaveBeenCalled();
261+
expect(result).toBeDefined();
262+
});
263+
});
264+
});

0 commit comments

Comments
 (0)