Skip to content

Commit 49b0c92

Browse files
feature(#20): this commit introduces end-to-end tests for a client's audience removal endpoint
1 parent 226a04d commit 49b0c92

1 file changed

Lines changed: 265 additions & 0 deletions

File tree

Applications/Backend/Tests/Integration/Endpoints/ClientEndpointTests.cs

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)