@@ -298,6 +298,203 @@ void agentCca_AppAndUserTokens_CacheIsolation() throws Exception {
298298 "Cache should have at least 2 entries (app + user)" );
299299 }
300300
301+ // ========================================================================
302+ // High-level AcquireTokenForAgent tests (composite API)
303+ // ========================================================================
304+
305+ /**
306+ * Tests the high-level acquireTokenForAgent API with a UPN-based AgentIdentity.
307+ * Exercises the full 3-leg flow orchestrated internally by AcquireTokenForAgentSupplier.
308+ */
309+ @ Test
310+ void acquireTokenForAgent_withUpn_fullFlow () throws Exception {
311+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
312+
313+ ConfidentialClientApplication blueprintCca = ConfidentialClientApplication .builder (
314+ BLUEPRINT_CLIENT_ID , clientCert )
315+ .authority (AUTHORITY )
316+ .sendX5c (true )
317+ .azureRegion (AZURE_REGION )
318+ .build ();
319+
320+ AgentIdentity agentId = AgentIdentity .withUsername (AGENT_APP_ID , USER_UPN );
321+
322+ IAuthenticationResult result = blueprintCca .acquireTokenForAgent (
323+ AcquireTokenForAgentParameters .builder (
324+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
325+ ).get ();
326+
327+ assertNotNull (result , "Result should not be null" );
328+ assertNotNull (result .accessToken (), "Access token should not be null" );
329+ assertFalse (result .accessToken ().isEmpty (), "Access token should not be empty" );
330+ assertNotNull (result .account (), "Account should not be null for user token" );
331+ }
332+
333+ /**
334+ * Tests the high-level acquireTokenForAgent API for app-only (no user) scenarios.
335+ * Only Legs 1-2 are performed.
336+ */
337+ @ Test
338+ void acquireTokenForAgent_appOnly () throws Exception {
339+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
340+
341+ ConfidentialClientApplication blueprintCca = ConfidentialClientApplication .builder (
342+ BLUEPRINT_CLIENT_ID , clientCert )
343+ .authority (AUTHORITY )
344+ .sendX5c (true )
345+ .azureRegion (AZURE_REGION )
346+ .build ();
347+
348+ AgentIdentity agentId = AgentIdentity .appOnly (AGENT_APP_ID );
349+
350+ IAuthenticationResult result = blueprintCca .acquireTokenForAgent (
351+ AcquireTokenForAgentParameters .builder (
352+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
353+ ).get ();
354+
355+ assertNotNull (result , "Result should not be null" );
356+ assertNotNull (result .accessToken (), "Access token should not be null" );
357+ assertFalse (result .accessToken ().isEmpty (), "Access token should not be empty" );
358+ }
359+
360+ /**
361+ * Tests the high-level acquireTokenForAgent API with ForceRefresh.
362+ * First call populates cache, second call (forceRefresh) bypasses it.
363+ */
364+ @ Test
365+ void acquireTokenForAgent_forceRefresh () throws Exception {
366+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
367+
368+ ConfidentialClientApplication blueprintCca = ConfidentialClientApplication .builder (
369+ BLUEPRINT_CLIENT_ID , clientCert )
370+ .authority (AUTHORITY )
371+ .sendX5c (true )
372+ .azureRegion (AZURE_REGION )
373+ .build ();
374+
375+ AgentIdentity agentId = AgentIdentity .withUsername (AGENT_APP_ID , USER_UPN );
376+
377+ // First call — populates cache
378+ IAuthenticationResult result1 = blueprintCca .acquireTokenForAgent (
379+ AcquireTokenForAgentParameters .builder (
380+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
381+ ).get ();
382+ assertNotNull (result1 .accessToken ());
383+
384+ // Second call without forceRefresh — should return cached token
385+ IAuthenticationResult result2 = blueprintCca .acquireTokenForAgent (
386+ AcquireTokenForAgentParameters .builder (
387+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
388+ ).get ();
389+ assertEquals (result1 .accessToken (), result2 .accessToken (),
390+ "Second call should return cached token" );
391+
392+ // Third call with forceRefresh — should get a fresh token
393+ IAuthenticationResult result3 = blueprintCca .acquireTokenForAgent (
394+ AcquireTokenForAgentParameters .builder (
395+ Collections .singleton (GRAPH_SCOPE ), agentId )
396+ .forceRefresh (true ).build ()
397+ ).get ();
398+ assertNotNull (result3 .accessToken ());
399+ // The fresh token may be the same string (if not expired) but the flow exercised network
400+ }
401+
402+ /**
403+ * Tests cache isolation between two blueprint CCA instances.
404+ * Each blueprint should have its own agent CCA cache.
405+ */
406+ @ Test
407+ void acquireTokenForAgent_cacheIsolation_twoBlueprintCcas () throws Exception {
408+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
409+
410+ ConfidentialClientApplication blueprint1 = ConfidentialClientApplication .builder (
411+ BLUEPRINT_CLIENT_ID , clientCert )
412+ .authority (AUTHORITY )
413+ .sendX5c (true )
414+ .azureRegion (AZURE_REGION )
415+ .build ();
416+
417+ ConfidentialClientApplication blueprint2 = ConfidentialClientApplication .builder (
418+ BLUEPRINT_CLIENT_ID , clientCert )
419+ .authority (AUTHORITY )
420+ .sendX5c (true )
421+ .azureRegion (AZURE_REGION )
422+ .build ();
423+
424+ AgentIdentity agentId = AgentIdentity .withUsername (AGENT_APP_ID , USER_UPN );
425+
426+ // Acquire via blueprint1
427+ IAuthenticationResult result1 = blueprint1 .acquireTokenForAgent (
428+ AcquireTokenForAgentParameters .builder (
429+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
430+ ).get ();
431+ assertNotNull (result1 .accessToken ());
432+
433+ // Blueprint1 should have agent CCA cached, blueprint2 should not
434+ assertEquals (1 , blueprint1 .agentCcaCache .size (),
435+ "Blueprint1 should have one cached agent CCA" );
436+ assertTrue (blueprint2 .agentCcaCache .isEmpty (),
437+ "Blueprint2 should have no cached agent CCAs (no bleed)" );
438+
439+ // Acquire via blueprint2
440+ IAuthenticationResult result2 = blueprint2 .acquireTokenForAgent (
441+ AcquireTokenForAgentParameters .builder (
442+ Collections .singleton (GRAPH_SCOPE ), agentId ).build ()
443+ ).get ();
444+ assertNotNull (result2 .accessToken ());
445+
446+ // Both should now have their own cache entries
447+ assertEquals (1 , blueprint1 .agentCcaCache .size ());
448+ assertEquals (1 , blueprint2 .agentCcaCache .size ());
449+ }
450+
451+ /**
452+ * Tests that a UPN-based token can be found by OID lookup on the same blueprint.
453+ * Discovers the OID via the UPN flow, then verifies OID-based call returns cached token.
454+ */
455+ @ Test
456+ void acquireTokenForAgent_upnThenOid_sharesCache () throws Exception {
457+ IClientCertificate clientCert = ClientCredentialFactory .createFromCertificate (privateKey , certificate );
458+
459+ ConfidentialClientApplication blueprintCca = ConfidentialClientApplication .builder (
460+ BLUEPRINT_CLIENT_ID , clientCert )
461+ .authority (AUTHORITY )
462+ .sendX5c (true )
463+ .azureRegion (AZURE_REGION )
464+ .build ();
465+
466+ // Step 1: Acquire via UPN
467+ AgentIdentity upnIdentity = AgentIdentity .withUsername (AGENT_APP_ID , USER_UPN );
468+ IAuthenticationResult upnResult = blueprintCca .acquireTokenForAgent (
469+ AcquireTokenForAgentParameters .builder (
470+ Collections .singleton (GRAPH_SCOPE ), upnIdentity ).build ()
471+ ).get ();
472+ assertNotNull (upnResult .account (), "Account should not be null" );
473+
474+ // Extract OID from account's homeAccountId (format: oid.tid)
475+ String homeAccountId = upnResult .account ().homeAccountId ();
476+ assertNotNull (homeAccountId );
477+ String oidString = homeAccountId .contains ("." )
478+ ? homeAccountId .substring (0 , homeAccountId .indexOf ('.' ))
479+ : homeAccountId ;
480+ java .util .UUID userOid = java .util .UUID .fromString (oidString );
481+
482+ // Step 2: Acquire via OID — should come from cache
483+ AgentIdentity oidIdentity = new AgentIdentity (AGENT_APP_ID , userOid );
484+ IAuthenticationResult oidResult = blueprintCca .acquireTokenForAgent (
485+ AcquireTokenForAgentParameters .builder (
486+ Collections .singleton (GRAPH_SCOPE ), oidIdentity ).build ()
487+ ).get ();
488+
489+ // Should return the same cached token
490+ assertEquals (upnResult .accessToken (), oidResult .accessToken (),
491+ "OID-based call should return the same cached token as UPN-based call" );
492+ }
493+
494+ // ========================================================================
495+ // Helpers
496+ // ========================================================================
497+
301498 /**
302499 * Helper: acquires an FMI credential from the RMA (Resource Management Application).
303500 * Uses FMI_EXCHANGE_SCOPE, matching FmiIT's Flow3 pattern.
0 commit comments