Skip to content

Commit 7707498

Browse files
committed
Add tests
1 parent 30f4bd9 commit 7707498

File tree

2 files changed

+270
-2
lines changed

2 files changed

+270
-2
lines changed

packages/apps/src/app.spec.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class TestApp extends App {
2525
public async testGetAppGraphToken(tenantId?: string) {
2626
return this.getAppGraphToken(tenantId);
2727
}
28+
29+
public getCredentials() {
30+
return this.credentials;
31+
}
2832
}
2933

3034
describe('App', () => {
@@ -180,4 +184,151 @@ describe('App', () => {
180184
expect(mockAcquireToken).not.toHaveBeenCalled();
181185
});
182186
});
187+
188+
describe('Credentials', () => {
189+
const originalEnv = process.env;
190+
191+
beforeEach(() => {
192+
jest.resetModules();
193+
process.env = { ...originalEnv };
194+
});
195+
196+
afterEach(() => {
197+
process.env = originalEnv;
198+
});
199+
200+
it('should create ClientCredentials with clientSecret from options or env', () => {
201+
// From options
202+
const appWithSecret = new TestApp({
203+
clientId: 'test-client-id',
204+
clientSecret: 'test-client-secret',
205+
tenantId: 'test-tenant-id',
206+
plugins: [new TestHttpPlugin()],
207+
});
208+
209+
expect(appWithSecret.getCredentials()).toEqual({
210+
type: 'clientSecret',
211+
clientId: 'test-client-id',
212+
clientSecret: 'test-client-secret',
213+
tenantId: 'test-tenant-id',
214+
});
215+
216+
// From environment variables
217+
process.env.CLIENT_ID = 'env-client-id';
218+
process.env.CLIENT_SECRET = 'env-client-secret';
219+
process.env.TENANT_ID = 'env-tenant-id';
220+
221+
const appFromEnv = new TestApp({
222+
plugins: [new TestHttpPlugin()],
223+
});
224+
225+
expect(appFromEnv.getCredentials()).toEqual({
226+
type: 'clientSecret',
227+
clientId: 'env-client-id',
228+
clientSecret: 'env-client-secret',
229+
tenantId: 'env-tenant-id',
230+
});
231+
});
232+
233+
it('should create TokenCredentials with token provider', () => {
234+
const tokenProvider = jest.fn().mockResolvedValue('mock-token');
235+
const appWithToken = new TestApp({
236+
clientId: 'test-client-id',
237+
token: tokenProvider,
238+
tenantId: 'test-tenant-id',
239+
plugins: [new TestHttpPlugin()],
240+
});
241+
242+
expect(appWithToken.getCredentials()).toEqual({
243+
type: 'token',
244+
clientId: 'test-client-id',
245+
token: tokenProvider,
246+
tenantId: 'test-tenant-id',
247+
});
248+
});
249+
250+
it('should create UserManagedIdentity credentials with only clientId from options or env', () => {
251+
// From options
252+
const appWithUMI = new TestApp({
253+
clientId: 'test-client-id',
254+
tenantId: 'test-tenant-id',
255+
plugins: [new TestHttpPlugin()],
256+
});
257+
258+
expect(appWithUMI.getCredentials()).toEqual({
259+
type: 'userManagedIdentity',
260+
clientId: 'test-client-id',
261+
tenantId: 'test-tenant-id',
262+
});
263+
264+
// From environment variables
265+
process.env.CLIENT_ID = 'env-client-id';
266+
process.env.TENANT_ID = 'env-tenant-id';
267+
268+
const appFromEnv = new TestApp({
269+
plugins: [new TestHttpPlugin()],
270+
});
271+
272+
expect(appFromEnv.getCredentials()).toEqual({
273+
type: 'userManagedIdentity',
274+
clientId: 'env-client-id',
275+
tenantId: 'env-tenant-id',
276+
});
277+
});
278+
279+
it('should prioritize clientSecret over token when both provided', () => {
280+
const tokenProvider = jest.fn().mockResolvedValue('mock-token');
281+
const appWithBoth = new TestApp({
282+
clientId: 'test-client-id',
283+
clientSecret: 'test-client-secret',
284+
token: tokenProvider,
285+
tenantId: 'test-tenant-id',
286+
plugins: [new TestHttpPlugin()],
287+
});
288+
289+
expect(appWithBoth.getCredentials()).toEqual({
290+
type: 'clientSecret',
291+
clientId: 'test-client-id',
292+
clientSecret: 'test-client-secret',
293+
tenantId: 'test-tenant-id',
294+
});
295+
});
296+
297+
it('should merge options and environment variables', () => {
298+
// Options take precedence over env
299+
process.env.CLIENT_ID = 'env-client-id';
300+
process.env.CLIENT_SECRET = 'env-client-secret';
301+
process.env.TENANT_ID = 'env-tenant-id';
302+
303+
const appWithOptions = new TestApp({
304+
clientId: 'options-client-id',
305+
clientSecret: 'options-client-secret',
306+
tenantId: 'options-tenant-id',
307+
plugins: [new TestHttpPlugin()],
308+
});
309+
310+
expect(appWithOptions.getCredentials()).toEqual({
311+
type: 'clientSecret',
312+
clientId: 'options-client-id',
313+
clientSecret: 'options-client-secret',
314+
tenantId: 'options-tenant-id',
315+
});
316+
317+
// Mix options and env
318+
process.env.CLIENT_SECRET = 'env-client-secret';
319+
process.env.TENANT_ID = 'env-tenant-id';
320+
321+
const appMerged = new TestApp({
322+
clientId: 'options-client-id',
323+
plugins: [new TestHttpPlugin()],
324+
});
325+
326+
expect(appMerged.getCredentials()).toEqual({
327+
type: 'clientSecret',
328+
clientId: 'options-client-id',
329+
clientSecret: 'env-client-secret',
330+
tenantId: 'env-tenant-id',
331+
});
332+
});
333+
});
183334
});

packages/apps/src/token-manager.spec.ts

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { AuthenticationResult, ConfidentialClientApplication } from '@azure/msal-node';
1+
import { AuthenticationResult, ConfidentialClientApplication, ManagedIdentityApplication } from '@azure/msal-node';
22
import { type MockedObject } from 'jest-mock';
33

4-
import { ClientCredentials, TokenCredentials } from '@microsoft/teams.api';
4+
import { ClientCredentials, TokenCredentials, UserManagedIdentityCredentials } from '@microsoft/teams.api';
55
import { ConsoleLogger } from '@microsoft/teams.common';
66

77
import { TokenManager } from './token-manager';
@@ -50,6 +50,7 @@ describe('TokenManager', () => {
5050
let logger: ConsoleLogger;
5151

5252
const mockClientCredentials: ClientCredentials = {
53+
type: 'clientSecret',
5354
clientId: 'test-client-id',
5455
clientSecret: 'test-client-secret',
5556
tenantId: 'test-tenant-id'
@@ -97,6 +98,7 @@ describe('TokenManager', () => {
9798

9899
it('should use default bot framework tenant when credentials have no tenantId', async () => {
99100
const credentialsWithoutTenant: ClientCredentials = {
101+
type: 'clientSecret',
100102
clientId: 'test-client-id',
101103
clientSecret: 'test-client-secret'
102104
};
@@ -172,6 +174,7 @@ describe('TokenManager', () => {
172174

173175
it('should use default common tenant when no tenant is specified', async () => {
174176
const credentialsWithoutTenant: ClientCredentials = {
177+
type: 'clientSecret',
175178
clientId: 'test-client-id',
176179
clientSecret: 'test-client-secret'
177180
};
@@ -247,6 +250,7 @@ describe('TokenManager', () => {
247250
it('should use token provider for bot token when TokenCredentials provided', async () => {
248251
const mockTokenProvider = jest.fn().mockResolvedValue('mock-provider-token');
249252
const tokenCredentials: TokenCredentials = {
253+
type: 'token',
250254
clientId: 'test-client-id',
251255
token: mockTokenProvider,
252256
tenantId: 'test-tenant-id'
@@ -269,6 +273,7 @@ describe('TokenManager', () => {
269273
it('should use token provider for graph token when TokenCredentials provided', async () => {
270274
const mockTokenProvider = jest.fn().mockResolvedValue('mock-graph-provider-token');
271275
const tokenCredentials: TokenCredentials = {
276+
type: 'token',
272277
clientId: 'test-client-id',
273278
token: mockTokenProvider,
274279
tenantId: 'test-tenant-id'
@@ -291,6 +296,7 @@ describe('TokenManager', () => {
291296
it('should use default tenant for token provider when no tenant specified', async () => {
292297
const mockTokenProvider = jest.fn().mockResolvedValue('mock-token');
293298
const tokenCredentials: TokenCredentials = {
299+
type: 'token',
294300
clientId: 'test-client-id',
295301
token: mockTokenProvider
296302
};
@@ -309,6 +315,7 @@ describe('TokenManager', () => {
309315
it('should prioritize explicit tenant ID over credentials tenant ID', async () => {
310316
const mockTokenProvider = jest.fn().mockResolvedValue('mock-token');
311317
const tokenCredentials: TokenCredentials = {
318+
type: 'token',
312319
clientId: 'test-client-id',
313320
token: mockTokenProvider,
314321
tenantId: 'credentials-tenant'
@@ -326,6 +333,7 @@ describe('TokenManager', () => {
326333
it('should use credentials tenant ID when explicit tenant not provided', async () => {
327334
const mockTokenProvider = jest.fn().mockResolvedValue('mock-token');
328335
const tokenCredentials: TokenCredentials = {
336+
type: 'token',
329337
clientId: 'test-client-id',
330338
token: mockTokenProvider,
331339
tenantId: 'credentials-tenant'
@@ -343,6 +351,7 @@ describe('TokenManager', () => {
343351
it('should use default tenant when neither explicit nor credentials tenant provided', async () => {
344352
const mockTokenProvider = jest.fn().mockResolvedValue('mock-token');
345353
const tokenCredentials: TokenCredentials = {
354+
type: 'token',
346355
clientId: 'test-client-id',
347356
token: mockTokenProvider
348357
};
@@ -379,6 +388,7 @@ describe('TokenManager', () => {
379388
const providerError = new Error('Token provider failed');
380389
const mockTokenProvider = jest.fn().mockRejectedValue(providerError);
381390
const tokenCredentials: TokenCredentials = {
391+
type: 'token',
382392
clientId: 'test-client-id',
383393
token: mockTokenProvider,
384394
tenantId: 'test-tenant-id'
@@ -389,4 +399,111 @@ describe('TokenManager', () => {
389399
await expect(tokenManager.getBotToken()).rejects.toThrow('Token provider failed');
390400
});
391401
});
402+
403+
describe('UserManagedIdentityCredentials', () => {
404+
let mockManagedIdentityClient: MockedObject<ManagedIdentityApplication>;
405+
let mockAcquireToken: jest.Mock;
406+
407+
const mockUMICredentials: UserManagedIdentityCredentials = {
408+
type: 'userManagedIdentity',
409+
clientId: 'test-client-id',
410+
tenantId: 'test-tenant-id'
411+
};
412+
413+
beforeEach(() => {
414+
// Mock the acquireToken method
415+
mockAcquireToken = jest.fn();
416+
417+
// Mock the ManagedIdentityApplication instance
418+
mockManagedIdentityClient = {
419+
acquireToken: mockAcquireToken
420+
} as unknown as MockedObject<ManagedIdentityApplication>;
421+
422+
// Mock the ManagedIdentityApplication constructor
423+
(ManagedIdentityApplication as jest.MockedClass<typeof ManagedIdentityApplication>).mockImplementation(() => mockManagedIdentityClient);
424+
});
425+
426+
it('should acquire bot token via ManagedIdentityApplication', async () => {
427+
mockAcquireToken.mockResolvedValue(createMockAuthResult('mock-umi-bot-token'));
428+
429+
const tokenManager = new TokenManager(mockUMICredentials, logger);
430+
const token = await tokenManager.getBotToken();
431+
432+
expect(ManagedIdentityApplication).toHaveBeenCalledWith({
433+
managedIdentityIdParams: {
434+
userAssignedClientId: 'test-client-id'
435+
}
436+
});
437+
438+
expect(mockAcquireToken).toHaveBeenCalledWith({
439+
resource: 'https://api.botframework.com'
440+
});
441+
442+
expect(token).not.toBeNull();
443+
expect(token?.toString()).toBe('mock-umi-bot-token');
444+
});
445+
446+
it('should acquire graph token via ManagedIdentityApplication', async () => {
447+
mockAcquireToken.mockResolvedValue(createMockAuthResult('mock-umi-graph-token'));
448+
449+
const tokenManager = new TokenManager(mockUMICredentials, logger);
450+
const token = await tokenManager.getGraphToken();
451+
452+
expect(ManagedIdentityApplication).toHaveBeenCalledWith({
453+
managedIdentityIdParams: {
454+
userAssignedClientId: 'test-client-id'
455+
}
456+
});
457+
458+
expect(mockAcquireToken).toHaveBeenCalledWith({
459+
resource: 'https://graph.microsoft.com'
460+
});
461+
462+
expect(token).not.toBeNull();
463+
expect(token?.toString()).toBe('mock-umi-graph-token');
464+
});
465+
466+
it('should strip /.default suffix from scope when acquiring token', async () => {
467+
mockAcquireToken.mockResolvedValue(createMockAuthResult('mock-token'));
468+
469+
const tokenManager = new TokenManager(mockUMICredentials, logger);
470+
await tokenManager.getBotToken();
471+
472+
// Verify that /.default was stripped from the scope
473+
expect(mockAcquireToken).toHaveBeenCalledWith({
474+
resource: 'https://api.botframework.com'
475+
});
476+
});
477+
478+
it('should cache and reuse ManagedIdentityApplication instance', async () => {
479+
mockAcquireToken.mockResolvedValue(createMockAuthResult('mock-token'));
480+
481+
const tokenManager = new TokenManager(mockUMICredentials, logger);
482+
483+
// First call - should create new client
484+
await tokenManager.getBotToken();
485+
expect(ManagedIdentityApplication).toHaveBeenCalledTimes(1);
486+
487+
// Second call - should reuse cached client
488+
await tokenManager.getGraphToken();
489+
expect(ManagedIdentityApplication).toHaveBeenCalledTimes(1);
490+
});
491+
492+
it('should throw error when MSAL returns null', async () => {
493+
mockAcquireToken.mockResolvedValue(null);
494+
495+
const tokenManager = new TokenManager(mockUMICredentials, logger);
496+
497+
await expect(tokenManager.getBotToken()).rejects.toThrow('Failed to get token');
498+
});
499+
500+
it('should propagate MSAL errors', async () => {
501+
const msalError = new Error('Managed identity authentication failed');
502+
mockAcquireToken.mockRejectedValue(msalError);
503+
504+
const tokenManager = new TokenManager(mockUMICredentials, logger);
505+
506+
await expect(tokenManager.getGraphToken()).rejects.toThrow('Managed identity authentication failed');
507+
});
508+
});
392509
});

0 commit comments

Comments
 (0)