@@ -1405,9 +1405,10 @@ describe('McpToolServerConfigurationService', () => {
14051405 expect ( servers [ 0 ] . headers ?. Authorization ) . toBe ( `Bearer ${ mockToken } ` ) ;
14061406 } ) ;
14071407
1408- it ( 'should pass audience and scope through from gateway into MCPServerConfig (legacy path)' , async ( ) => {
1409- // Uses legacy path so attachPerAudienceTokens is not called — pure field passthrough check
1408+ it ( 'should pass audience and scope through from gateway into MCPServerConfig (TurnContext path)' , async ( ) => {
1409+ // Verifies gateway response fields are preserved end-to-end using the preferred TurnContext path.
14101410 const v2Audience = 'eeeeffff-0000-1111-2222-333344445555' ;
1411+ const mockToken = createMockJwt ( 'v2-fields' ) ;
14111412 // eslint-disable-next-line @typescript-eslint/no-require-imports
14121413 const axios = require ( 'axios' ) ;
14131414 jest . spyOn ( axios , 'get' ) . mockResolvedValue ( {
@@ -1418,11 +1419,129 @@ describe('McpToolServerConfigurationService', () => {
14181419 scope : 'Tools.ListInvoke.All'
14191420 } ]
14201421 } ) ;
1422+ getAgenticUserTokenSpy . mockResolvedValue ( mockToken ) ;
14211423
1422- const servers = await service . listToolServers ( 'agent-id' , 'mock-auth-token' ) ;
1424+ const servers = await service . listToolServers ( mockContext , mockAuthorization , 'graph' , mockToken ) ;
14231425
14241426 expect ( servers [ 0 ] . audience ) . toBe ( v2Audience ) ;
14251427 expect ( servers [ 0 ] . scope ) . toBe ( 'Tools.ListInvoke.All' ) ;
14261428 } ) ;
14271429 } ) ;
1430+
1431+ describe ( 'listToolServers legacy path — per-audience token attachment' , ( ) => {
1432+ const createMockJwt = ( ) => {
1433+ const header = Buffer . from ( JSON . stringify ( { alg : 'HS256' , typ : 'JWT' } ) ) . toString ( 'base64url' ) ;
1434+ const payload = Buffer . from ( JSON . stringify ( { exp : Math . floor ( Date . now ( ) / 1000 ) + 3600 } ) ) . toString ( 'base64url' ) ;
1435+ return `${ header } .${ payload } .mock-sig` ;
1436+ } ;
1437+
1438+ afterEach ( ( ) => {
1439+ delete process . env . BEARER_TOKEN ;
1440+ delete process . env . BEARER_TOKEN_MAILSERVER ;
1441+ delete process . env . BEARER_TOKEN_V2SERVER ;
1442+ } ) ;
1443+
1444+ describe ( 'dev mode (manifest)' , ( ) => {
1445+ beforeEach ( ( ) => { process . env . NODE_ENV = 'Development' ; } ) ;
1446+
1447+ it ( 'should attach BEARER_TOKEN for a V1 server when BEARER_TOKEN is set' , async ( ) => {
1448+ process . env . BEARER_TOKEN = 'shared-dev-token' ;
1449+ const manifestContent = {
1450+ mcpServers : [ { mcpServerName : 'mailServer' , url : 'http://localhost:3000' } ]
1451+ } ;
1452+ jest . spyOn ( fs , 'existsSync' ) . mockReturnValue ( true ) ;
1453+ jest . spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( manifestContent ) ) ;
1454+
1455+ const servers = await service . listToolServers ( 'agent-id' , 'mock-auth-token' ) ;
1456+
1457+ expect ( servers [ 0 ] . headers ?. Authorization ) . toBe ( 'Bearer shared-dev-token' ) ;
1458+ } ) ;
1459+
1460+ it ( 'should attach BEARER_TOKEN_<NAME> for a V2 server when per-server env var is set' , async ( ) => {
1461+ process . env . BEARER_TOKEN_V2SERVER = 'v2-dev-token' ;
1462+ const v2Audience = 'aaaabbbb-1234-5678-abcd-111122223333' ;
1463+ const manifestContent = {
1464+ mcpServers : [ { mcpServerName : 'v2Server' , url : 'http://localhost:3001' , audience : v2Audience } ]
1465+ } ;
1466+ jest . spyOn ( fs , 'existsSync' ) . mockReturnValue ( true ) ;
1467+ jest . spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( manifestContent ) ) ;
1468+
1469+ const servers = await service . listToolServers ( 'agent-id' , 'mock-auth-token' ) ;
1470+
1471+ expect ( servers [ 0 ] . headers ?. Authorization ) . toBe ( 'Bearer v2-dev-token' ) ;
1472+ } ) ;
1473+
1474+ it ( 'should leave headers undefined when no env var tokens are set' , async ( ) => {
1475+ const manifestContent = {
1476+ mcpServers : [ { mcpServerName : 'mailServer' , url : 'http://localhost:3000' } ]
1477+ } ;
1478+ jest . spyOn ( fs , 'existsSync' ) . mockReturnValue ( true ) ;
1479+ jest . spyOn ( fs , 'readFileSync' ) . mockReturnValue ( JSON . stringify ( manifestContent ) ) ;
1480+
1481+ const servers = await service . listToolServers ( 'agent-id' , 'mock-auth-token' ) ;
1482+
1483+ expect ( servers [ 0 ] . headers ?. Authorization ) . toBeUndefined ( ) ;
1484+ } ) ;
1485+ } ) ;
1486+
1487+ describe ( 'prod mode (gateway)' , ( ) => {
1488+ beforeEach ( ( ) => {
1489+ process . env . NODE_ENV = 'production' ;
1490+ jest . spyOn ( Utility , 'ValidateAuthToken' ) . mockImplementation ( ( ) => { } ) ;
1491+ } ) ;
1492+
1493+ it ( 'should attach the shared authToken for a V1 server (no audience)' , async ( ) => {
1494+ const mockToken = createMockJwt ( ) ;
1495+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1496+ const axios = require ( 'axios' ) ;
1497+ jest . spyOn ( axios , 'get' ) . mockResolvedValue ( {
1498+ data : [ { mcpServerName : 'mailServer' , url : 'http://prod.example.com' } ]
1499+ } ) ;
1500+
1501+ const servers = await service . listToolServers ( 'agent-id' , mockToken ) ;
1502+
1503+ expect ( servers [ 0 ] . headers ?. Authorization ) . toBe ( `Bearer ${ mockToken } ` ) ;
1504+ } ) ;
1505+
1506+ it ( 'should throw for a V2 server (non-ATG audience) with a message directing to the TurnContext overload' , async ( ) => {
1507+ const v2Audience = 'aaaabbbb-1234-5678-abcd-111122223333' ;
1508+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1509+ const axios = require ( 'axios' ) ;
1510+ jest . spyOn ( axios , 'get' ) . mockResolvedValue ( {
1511+ data : [ { mcpServerName : 'v2Server' , url : 'http://v2.example.com' , audience : v2Audience } ]
1512+ } ) ;
1513+
1514+ await expect (
1515+ service . listToolServers ( 'agent-id' , 'mock-auth-token' )
1516+ ) . rejects . toThrow ( "MCP server 'v2Server' requires a per-audience token" ) ;
1517+ } ) ;
1518+
1519+ it ( 'should throw with a message that names the migration overload' , async ( ) => {
1520+ const v2Audience = 'ccccdddd-5678-9012-efab-444455556666' ;
1521+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1522+ const axios = require ( 'axios' ) ;
1523+ jest . spyOn ( axios , 'get' ) . mockResolvedValue ( {
1524+ data : [ { mcpServerName : 'v2Tools' , url : 'http://v2tools.example.com' , audience : v2Audience } ]
1525+ } ) ;
1526+
1527+ await expect (
1528+ service . listToolServers ( 'agent-id' , 'mock-auth-token' )
1529+ ) . rejects . toThrow ( 'listToolServers(turnContext, authorization, authHandlerName)' ) ;
1530+ } ) ;
1531+
1532+ it ( 'should NOT throw for a V1 server whose audience explicitly equals the shared ATG AppId' , async ( ) => {
1533+ const atgAppId = 'ea9ffc3e-8a23-4a7d-836d-234d7c7565c1' ;
1534+ const mockToken = createMockJwt ( ) ;
1535+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1536+ const axios = require ( 'axios' ) ;
1537+ jest . spyOn ( axios , 'get' ) . mockResolvedValue ( {
1538+ data : [ { mcpServerName : 'legacyServer' , url : 'http://legacy.example.com' , audience : atgAppId } ]
1539+ } ) ;
1540+
1541+ const servers = await service . listToolServers ( 'agent-id' , mockToken ) ;
1542+
1543+ expect ( servers [ 0 ] . headers ?. Authorization ) . toBe ( `Bearer ${ mockToken } ` ) ;
1544+ } ) ;
1545+ } ) ;
1546+ } ) ;
14281547} ) ;
0 commit comments