1- import { AuthenticationResult , ConfidentialClientApplication } from '@azure/msal-node' ;
1+ import { AuthenticationResult , ConfidentialClientApplication , ManagedIdentityApplication } from '@azure/msal-node' ;
22import { type MockedObject } from 'jest-mock' ;
33
4- import { ClientCredentials , TokenCredentials } from '@microsoft/teams.api' ;
4+ import { ClientCredentials , TokenCredentials , UserManagedIdentityCredentials } from '@microsoft/teams.api' ;
55import { ConsoleLogger } from '@microsoft/teams.common' ;
66
77import { 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