@@ -357,6 +357,15 @@ void fmiPath_WhitespaceOnlyThrowsIllegalArgumentException() {
357357 .build ());
358358 }
359359
360+ @ Test
361+ void fmiPath_NullValueThrowsIllegalArgumentException () {
362+ assertThrows (IllegalArgumentException .class , () ->
363+ ClientCredentialParameters
364+ .builder (Collections .singleton ("scope" ))
365+ .fmiPath (null )
366+ .build ());
367+ }
368+
360369 // ========================================================================
361370 // Exact cache key string validation
362371 // ========================================================================
@@ -451,4 +460,118 @@ void fmiPath_NoFmiPath_CacheKeyUsesAccessTokenCredentialType() throws Exception
451460 "Cache key without fmi_path should use 'accesstoken' credential type and no hash suffix" );
452461 }
453462
463+ // ========================================================================
464+ // Cache filter isolation: FMI tokens not returned for non-FMI requests (and vice versa)
465+ // ========================================================================
466+
467+ @ Test
468+ void fmiPath_CacheIsolation_FmiTokenNotReturnedForNonFmiRequest () throws Exception {
469+ // Seed cache with an FMI-tagged token, then verify a non-FMI request does NOT
470+ // return it from cache (goes to IdP instead).
471+ DefaultHttpClient httpClientMock = mock (DefaultHttpClient .class );
472+
473+ when (httpClientMock .send (any (HttpRequest .class ))).thenReturn (
474+ TestHelper .expectedResponse (HttpStatus .HTTP_OK ,
475+ TestHelper .getSuccessfulTokenResponse (new HashMap <>())));
476+
477+ ConfidentialClientApplication cca =
478+ ConfidentialClientApplication .builder ("clientId" ,
479+ ClientCredentialFactory .createFromSecret ("secret" ))
480+ .authority ("https://login.microsoftonline.com/tenant/" )
481+ .aadInstanceDiscoveryResponse (TestHelper .getInstanceDiscoveryResponse ())
482+ .httpClient (httpClientMock )
483+ .build ();
484+
485+ // First request WITH fmi_path — seeds cache with an FMI-tagged token
486+ ClientCredentialParameters fmiParams = ClientCredentialParameters
487+ .builder (Collections .singleton ("scope" ))
488+ .fmiPath ("agentApp1" )
489+ .build ();
490+ cca .acquireToken (fmiParams ).get ();
491+ assertEquals (1 , cca .tokenCache .accessTokens .size ());
492+
493+ // Second request WITHOUT fmi_path — should NOT get the FMI token from cache
494+ ClientCredentialParameters nonFmiParams = ClientCredentialParameters
495+ .builder (Collections .singleton ("scope" ))
496+ .build ();
497+ cca .acquireToken (nonFmiParams ).get ();
498+
499+ // Both tokens should now be in cache (FMI miss → went to IdP → stored as non-FMI)
500+ assertEquals (2 , cca .tokenCache .accessTokens .size (),
501+ "Non-FMI request should not match FMI-tagged cache entry; both tokens should exist" );
502+ }
503+
504+ @ Test
505+ void fmiPath_CacheIsolation_NonFmiTokenNotReturnedForFmiRequest () throws Exception {
506+ // Seed cache with a non-FMI token, then verify an FMI request does NOT
507+ // return it from cache (goes to IdP instead).
508+ DefaultHttpClient httpClientMock = mock (DefaultHttpClient .class );
509+
510+ when (httpClientMock .send (any (HttpRequest .class ))).thenReturn (
511+ TestHelper .expectedResponse (HttpStatus .HTTP_OK ,
512+ TestHelper .getSuccessfulTokenResponse (new HashMap <>())));
513+
514+ ConfidentialClientApplication cca =
515+ ConfidentialClientApplication .builder ("clientId" ,
516+ ClientCredentialFactory .createFromSecret ("secret" ))
517+ .authority ("https://login.microsoftonline.com/tenant/" )
518+ .aadInstanceDiscoveryResponse (TestHelper .getInstanceDiscoveryResponse ())
519+ .httpClient (httpClientMock )
520+ .build ();
521+
522+ // First request WITHOUT fmi_path — seeds cache with a non-FMI token
523+ ClientCredentialParameters nonFmiParams = ClientCredentialParameters
524+ .builder (Collections .singleton ("scope" ))
525+ .build ();
526+ cca .acquireToken (nonFmiParams ).get ();
527+ assertEquals (1 , cca .tokenCache .accessTokens .size ());
528+
529+ // Second request WITH fmi_path — should NOT get the non-FMI token from cache
530+ ClientCredentialParameters fmiParams = ClientCredentialParameters
531+ .builder (Collections .singleton ("scope" ))
532+ .fmiPath ("agentApp1" )
533+ .build ();
534+ cca .acquireToken (fmiParams ).get ();
535+
536+ // Both tokens should now be in cache (FMI miss → went to IdP → stored as FMI)
537+ assertEquals (2 , cca .tokenCache .accessTokens .size (),
538+ "FMI request should not match non-FMI cache entry; both tokens should exist" );
539+ }
540+
541+ @ Test
542+ void fmiPath_CacheIsolation_DifferentFmiPathsNotShared () throws Exception {
543+ // Two different fmi_path values should produce separate cache entries
544+ DefaultHttpClient httpClientMock = mock (DefaultHttpClient .class );
545+
546+ when (httpClientMock .send (any (HttpRequest .class ))).thenReturn (
547+ TestHelper .expectedResponse (HttpStatus .HTTP_OK ,
548+ TestHelper .getSuccessfulTokenResponse (new HashMap <>())));
549+
550+ ConfidentialClientApplication cca =
551+ ConfidentialClientApplication .builder ("clientId" ,
552+ ClientCredentialFactory .createFromSecret ("secret" ))
553+ .authority ("https://login.microsoftonline.com/tenant/" )
554+ .aadInstanceDiscoveryResponse (TestHelper .getInstanceDiscoveryResponse ())
555+ .httpClient (httpClientMock )
556+ .build ();
557+
558+ // Request with fmi_path "agentA"
559+ ClientCredentialParameters paramsA = ClientCredentialParameters
560+ .builder (Collections .singleton ("scope" ))
561+ .fmiPath ("agentA" )
562+ .build ();
563+ cca .acquireToken (paramsA ).get ();
564+
565+ // Request with fmi_path "agentB"
566+ ClientCredentialParameters paramsB = ClientCredentialParameters
567+ .builder (Collections .singleton ("scope" ))
568+ .fmiPath ("agentB" )
569+ .build ();
570+ cca .acquireToken (paramsB ).get ();
571+
572+ // Each fmi_path produces a different hash → different cache key → 2 entries
573+ assertEquals (2 , cca .tokenCache .accessTokens .size (),
574+ "Different fmi_path values should produce separate cache entries" );
575+ }
576+
454577}
0 commit comments