@@ -922,4 +922,269 @@ public async Task WhenPostClientAudiencesWithNonExistentClient_ShouldReturnNotFo
922922 Assert . Equal ( HttpStatusCode . NotFound , response . StatusCode ) ;
923923 Assert . Equal ( ClientErrors . ClientDoesNotExist , error ) ;
924924 }
925+
926+ [ Fact ( DisplayName = "[e2e] - when POST /clients/{id}/audiences and generating token should include audiences in JWT" ) ]
927+ public async Task WhenAssigningAudiencesAndGeneratingToken_ShouldIncludeAudiencesInJwt ( )
928+ {
929+ /* arrange: resolve required dependencies */
930+ var clientCollection = factory . Services . GetRequiredService < IClientCollection > ( ) ;
931+
932+ /* arrange: authenticate user and get access token */
933+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
934+ var credentials = new AuthenticationCredentials
935+ {
936+ Username = "federation.testing.user" ,
937+ Password = "federation.testing.password"
938+ } ;
939+
940+ var authenticationResponse = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , credentials ) ;
941+ var authenticationResult = await authenticationResponse . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
942+
943+ Assert . NotNull ( authenticationResult ) ;
944+ Assert . NotEmpty ( authenticationResult . AccessToken ) ;
945+
946+ httpClient . WithAuthorization ( authenticationResult . AccessToken ) ;
947+
948+ /* arrange: create a new client */
949+ var clientPayload = _fixture . Build < ClientCreationScheme > ( )
950+ . With ( client => client . Name , $ "test-client-{ Guid . NewGuid ( ) } ")
951+ . With ( client => client . Flows , [ Grant . ClientCredentials ] )
952+ . With ( client => client . RedirectUris , [ ] )
953+ . Create ( ) ;
954+
955+ var clientResponse = await httpClient . PostAsJsonAsync ( "api/v1/clients" , clientPayload ) ;
956+
957+ Assert . Equal ( HttpStatusCode . Created , clientResponse . StatusCode ) ;
958+
959+ var clientFilters = ClientFilters . WithSpecifications ( )
960+ . WithName ( clientPayload . Name )
961+ . Build ( ) ;
962+
963+ var clients = await clientCollection . GetClientsAsync ( clientFilters , CancellationToken . None ) ;
964+ var client = clients . FirstOrDefault ( ) ;
965+
966+ Assert . NotEmpty ( clients ) ;
967+ Assert . NotNull ( client ) ;
968+
969+ /* arrange: assign first audience */
970+ var audience1 = new AssignClientAudienceScheme
971+ {
972+ Value = "https://api1.example.com"
973+ } ;
974+
975+ var audience1Response = await httpClient . PostAsJsonAsync ( $ "api/v1/clients/{ client . Id } /audiences", audience1 ) ;
976+
977+ Assert . NotNull ( audience1Response ) ;
978+ Assert . Equal ( HttpStatusCode . OK , audience1Response . StatusCode ) ;
979+
980+ /* arrange: assign second audience */
981+ var audience2 = new AssignClientAudienceScheme
982+ {
983+ Value = "https://api2.example.com"
984+ } ;
985+
986+ var audience2Response = await httpClient . PostAsJsonAsync ( $ "api/v1/clients/{ client . Id } /audiences", audience2 ) ;
987+
988+ Assert . NotNull ( audience2Response ) ;
989+ Assert . Equal ( HttpStatusCode . OK , audience2Response . StatusCode ) ;
990+
991+ /* arrange: prepare client credentials for token generation */
992+ var connectClient = factory . HttpClient ;
993+ var clientCredentials = new Dictionary < string , string >
994+ {
995+ { "grant_type" , "client_credentials" } ,
996+ { "client_id" , client . ClientId } ,
997+ { "client_secret" , client . Secret }
998+ } ;
999+
1000+ var content = new FormUrlEncodedContent ( clientCredentials ) ;
1001+
1002+ /* act: generate token via client_credentials */
1003+ var tokenResponse = await connectClient . PostAsync ( "api/v1/protocol/open-id/connect/token" , content ) ;
1004+ var tokenResult = await tokenResponse . Content . ReadFromJsonAsync < ClientAuthenticationResult > ( ) ;
1005+
1006+ Assert . Equal ( HttpStatusCode . OK , tokenResponse . StatusCode ) ;
1007+
1008+ Assert . NotNull ( tokenResult ) ;
1009+ Assert . NotEmpty ( tokenResult . AccessToken ) ;
1010+
1011+ /* act: decode JWT to verify audiences */
1012+ var handler = new JwtSecurityTokenHandler ( ) ;
1013+ var token = handler . ReadJwtToken ( tokenResult . AccessToken ) ;
1014+
1015+ /* assert: token should contain both audiences in the 'aud' claim */
1016+ var audienceClaims = token . Claims
1017+ . Where ( claim => claim . Type == JwtRegisteredClaimNames . Aud )
1018+ . Select ( claim => claim . Value )
1019+ . ToList ( ) ;
1020+
1021+ Assert . NotNull ( audienceClaims ) ;
1022+ Assert . NotEmpty ( audienceClaims ) ;
1023+
1024+ Assert . Contains ( audience1 . Value , audienceClaims ) ;
1025+ Assert . Contains ( audience2 . Value , audienceClaims ) ;
1026+ }
1027+
1028+ [ Fact ( DisplayName = "[e2e] - when DELETE /clients/{id}/audiences/{audience} should revoke audience successfully" ) ]
1029+ public async Task WhenDeleteClientAudience_ShouldRevokeAudienceSuccessfully ( )
1030+ {
1031+ /* arrange: resolve required dependencies */
1032+ var clientCollection = factory . Services . GetRequiredService < IClientCollection > ( ) ;
1033+
1034+ /* arrange: authenticate user and get access token */
1035+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
1036+ var credentials = new AuthenticationCredentials
1037+ {
1038+ Username = "federation.testing.user" ,
1039+ Password = "federation.testing.password"
1040+ } ;
1041+
1042+ var authenticationResponse = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , credentials ) ;
1043+ var authenticationResult = await authenticationResponse . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
1044+
1045+ Assert . NotNull ( authenticationResult ) ;
1046+ Assert . NotEmpty ( authenticationResult . AccessToken ) ;
1047+
1048+ httpClient . WithAuthorization ( authenticationResult . AccessToken ) ;
1049+
1050+ /* arrange: create a new client */
1051+ var clientPayload = _fixture . Build < ClientCreationScheme > ( )
1052+ . With ( client => client . Name , $ "test-client-{ Guid . NewGuid ( ) } ")
1053+ . With ( client => client . Flows , [ Grant . ClientCredentials ] )
1054+ . With ( client => client . RedirectUris , [ ] )
1055+ . Create ( ) ;
1056+
1057+ var clientResponse = await httpClient . PostAsJsonAsync ( "api/v1/clients" , clientPayload ) ;
1058+
1059+ Assert . Equal ( HttpStatusCode . Created , clientResponse . StatusCode ) ;
1060+
1061+ var clientFilters = ClientFilters . WithSpecifications ( )
1062+ . WithName ( clientPayload . Name )
1063+ . Build ( ) ;
1064+
1065+ var clients = await clientCollection . GetClientsAsync ( clientFilters , CancellationToken . None ) ;
1066+ var client = clients . FirstOrDefault ( ) ;
1067+
1068+ Assert . NotEmpty ( clients ) ;
1069+ Assert . NotNull ( client ) ;
1070+
1071+ /* arrange: assign two audiences */
1072+ var audience1 = "https://api1.example.com" ;
1073+ var audience2 = "https://api2.example.com" ;
1074+
1075+ var audience1Payload = new AssignClientAudienceScheme { Value = audience1 } ;
1076+ var audience2Payload = new AssignClientAudienceScheme { Value = audience2 } ;
1077+
1078+ await httpClient . PostAsJsonAsync ( $ "api/v1/clients/{ client . Id } /audiences", audience1Payload ) ;
1079+ await httpClient . PostAsJsonAsync ( $ "api/v1/clients/{ client . Id } /audiences", audience2Payload ) ;
1080+
1081+ /* act: send DELETE request to revoke first audience */
1082+ var response = await httpClient . DeleteAsync ( $ "api/v1/clients/{ client . Id } /audiences/{ Uri . EscapeDataString ( audience1 ) } ") ;
1083+ var remainingAudiences = await response . Content . ReadFromJsonAsync < IReadOnlyCollection < string > > ( ) ;
1084+
1085+ /* assert: response should be 200 OK */
1086+ Assert . Equal ( HttpStatusCode . OK , response . StatusCode ) ;
1087+ Assert . NotNull ( remainingAudiences ) ;
1088+
1089+ /* assert: only second audience should remain */
1090+ Assert . Single ( remainingAudiences ) ;
1091+ Assert . Contains ( audience2 , remainingAudiences ) ;
1092+ Assert . DoesNotContain ( audience1 , remainingAudiences ) ;
1093+ }
1094+
1095+ [ Fact ( DisplayName = "[e2e] - when DELETE /clients/{id}/audiences/{audience} with non-existent client should return 404 #ERROR-2D943" ) ]
1096+ public async Task WhenDeleteClientAudienceWithNonExistentClient_ShouldReturnNotFound ( )
1097+ {
1098+ /* arrange: authenticate user and get access token */
1099+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
1100+ var credentials = new AuthenticationCredentials
1101+ {
1102+ Username = "federation.testing.user" ,
1103+ Password = "federation.testing.password"
1104+ } ;
1105+
1106+ var authenticationResponse = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , credentials ) ;
1107+ var authenticationResult = await authenticationResponse . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
1108+
1109+ Assert . NotNull ( authenticationResult ) ;
1110+ Assert . NotEmpty ( authenticationResult . AccessToken ) ;
1111+
1112+ httpClient . WithAuthorization ( authenticationResult . AccessToken ) ;
1113+
1114+ /* arrange: prepare request with non-existent client ID */
1115+ var nonExistentClientId = Guid . NewGuid ( ) . ToString ( ) ;
1116+ var audience = "https://api.example.com" ;
1117+
1118+ /* act: send DELETE request for non-existent client */
1119+ var response = await httpClient . DeleteAsync ( $ "api/v1/clients/{ nonExistentClientId } /audiences/{ Uri . EscapeDataString ( audience ) } ") ;
1120+ var error = await response . Content . ReadFromJsonAsync < Error > ( ) ;
1121+
1122+ /* assert: response should be 404 Not Found */
1123+ Assert . NotNull ( error ) ;
1124+
1125+ Assert . Equal ( HttpStatusCode . NotFound , response . StatusCode ) ;
1126+ Assert . Equal ( ClientErrors . ClientDoesNotExist , error ) ;
1127+ }
1128+
1129+ [ Fact ( DisplayName = "[e2e] - when DELETE /clients/{id}/audiences/{audience} with audience not assigned should return 409 #ERROR-B3F8E" ) ]
1130+ public async Task WhenDeleteClientAudienceWithAudienceNotAssigned_ShouldReturnConflict ( )
1131+ {
1132+ /* arrange: resolve required dependencies */
1133+ var clientCollection = factory . Services . GetRequiredService < IClientCollection > ( ) ;
1134+
1135+ /* arrange: authenticate user and get access token */
1136+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
1137+ var credentials = new AuthenticationCredentials
1138+ {
1139+ Username = "federation.testing.user" ,
1140+ Password = "federation.testing.password"
1141+ } ;
1142+
1143+ var authenticationResponse = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , credentials ) ;
1144+ var authenticationResult = await authenticationResponse . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
1145+
1146+ Assert . NotNull ( authenticationResult ) ;
1147+ Assert . NotEmpty ( authenticationResult . AccessToken ) ;
1148+
1149+ httpClient . WithAuthorization ( authenticationResult . AccessToken ) ;
1150+
1151+ /* arrange: create a new client */
1152+ var clientPayload = _fixture . Build < ClientCreationScheme > ( )
1153+ . With ( client => client . Name , $ "test-client-{ Guid . NewGuid ( ) } ")
1154+ . With ( client => client . Flows , [ Grant . ClientCredentials ] )
1155+ . With ( client => client . RedirectUris , [ ] )
1156+ . Create ( ) ;
1157+
1158+ var clientResponse = await httpClient . PostAsJsonAsync ( "api/v1/clients" , clientPayload ) ;
1159+
1160+ Assert . Equal ( HttpStatusCode . Created , clientResponse . StatusCode ) ;
1161+
1162+ var clientFilters = ClientFilters . WithSpecifications ( )
1163+ . WithName ( clientPayload . Name )
1164+ . Build ( ) ;
1165+
1166+ var clients = await clientCollection . GetClientsAsync ( clientFilters , CancellationToken . None ) ;
1167+ var client = clients . FirstOrDefault ( ) ;
1168+
1169+ Assert . NotEmpty ( clients ) ;
1170+ Assert . NotNull ( client ) ;
1171+
1172+ /* arrange: assign one audience */
1173+ var assignedAudience = "https://api1.example.com" ;
1174+ var assignPayload = new AssignClientAudienceScheme { Value = assignedAudience } ;
1175+
1176+ await httpClient . PostAsJsonAsync ( $ "api/v1/clients/{ client . Id } /audiences", assignPayload ) ;
1177+
1178+ /* act: send DELETE request for non-assigned audience */
1179+ var nonAssignedAudience = "https://api2.example.com" ;
1180+
1181+ var response = await httpClient . DeleteAsync ( $ "api/v1/clients/{ client . Id } /audiences/{ Uri . EscapeDataString ( nonAssignedAudience ) } ") ;
1182+ var error = await response . Content . ReadFromJsonAsync < Error > ( ) ;
1183+
1184+ /* assert: response should be 409 Conflict */
1185+ Assert . NotNull ( error ) ;
1186+
1187+ Assert . Equal ( HttpStatusCode . Conflict , response . StatusCode ) ;
1188+ Assert . Equal ( ClientErrors . AudienceNotAssigned , error ) ;
1189+ }
9251190}
0 commit comments