diff --git a/src/Turnierplan.App.Test.Functional/.config/dotnet-tools.json b/src/Turnierplan.App.Test.Functional/.config/dotnet-tools.json new file mode 100644 index 00000000..5b4d81ba --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.openapi.kiota": { + "version": "1.30.0", + "commands": [ + "kiota" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/Turnierplan.App.Test.Functional/.gitignore b/src/Turnierplan.App.Test.Functional/.gitignore new file mode 100644 index 00000000..c0febbdc --- /dev/null +++ b/src/Turnierplan.App.Test.Functional/.gitignore @@ -0,0 +1,2 @@ +# Generated HTTP clients +Client diff --git a/src/Turnierplan.App.Test.Functional/Routes.cs b/src/Turnierplan.App.Test.Functional/Routes.cs deleted file mode 100644 index 2d00b710..00000000 --- a/src/Turnierplan.App.Test.Functional/Routes.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Turnierplan.Core.PublicId; - -namespace Turnierplan.App.Test.Functional; - -internal static class Routes -{ - public static class ApiKeys - { - public static string Delete(PublicId id) => $"/api/api-keys/{id}"; - } - - public static class Identity - { - public static string Login() => "/api/identity/login"; - } - - public static class Organizations - { - public static string Create() => "/api/organizations"; - } - - public static class Users - { - public static string List() => "/api/users"; - public static string Create() => "/api/users"; - public static string Delete(Guid id) => $"/api/users/{id}"; - public static string Update(Guid id) => $"/api/users/{id}"; - } -} diff --git a/src/Turnierplan.App.Test.Functional/Scenarios.cs b/src/Turnierplan.App.Test.Functional/Scenarios.cs index 0d63df35..16ca2de7 100644 --- a/src/Turnierplan.App.Test.Functional/Scenarios.cs +++ b/src/Turnierplan.App.Test.Functional/Scenarios.cs @@ -1,15 +1,16 @@ using System.Net; -using System.Net.Http.Json; using FluentAssertions; using FluentAssertions.Extensions; -using Turnierplan.App.Models; +using Microsoft.Kiota.Abstractions; +using Turnierplan.App.Test.Functional.Client.Models; using Turnierplan.Core.ApiKey; using Turnierplan.Core.Extensions; using Turnierplan.Core.Organization; -using Turnierplan.Core.RoleAssignment; using Turnierplan.Core.Tournament; using Turnierplan.Core.User; using Xunit; +using Role = Turnierplan.Core.RoleAssignment.Role; +using Visibility = Turnierplan.Core.Tournament.Visibility; namespace Turnierplan.App.Test.Functional; @@ -46,14 +47,12 @@ public async Task When_ApiKey_And_User_Are_Deleted_The_Role_Assignments_Are_Also _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(2); - var resp = await _testServer.Client.DeleteAsync(Routes.ApiKeys.Delete(apiKeyId), TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); + await _testServer.Client.ApiKeys[apiKeyId].DeleteAsync(cancellationToken: TestContext.Current.CancellationToken); _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1); _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(1); - resp = await _testServer.Client.DeleteAsync(Routes.Users.Delete(userId), TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); + await _testServer.Client.Users[userId].DeleteAsync(cancellationToken: TestContext.Current.CancellationToken); _testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(0); _testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(0); @@ -65,36 +64,54 @@ public async Task New_User_Can_Not_Create_Organization_Unless_Explicitly_Granted const string newUserName = "test_user"; const string newUserPassword = "test123"; - var resp = await _testServer.Client.PostAsJsonAsync( - Routes.Users.Create(), - new { UserName = newUserName, Password = newUserPassword }, - TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); - - var userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword); - resp = await userClient.PostAsJsonAsync( - Routes.Organizations.Create(), - new { Name = "test_org" }, - TestContext.Current.CancellationToken); - resp.StatusCode.Should().Be(HttpStatusCode.Forbidden); - - // extra step required to get ID of new user - resp = await _testServer.Client.GetAsync(Routes.Users.List(), TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); - var allUsers = await resp.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); - var newUserId = allUsers!.Single(x => x.UserName.Equals(newUserName)).Id; - - resp = await _testServer.Client.PutAsJsonAsync( - Routes.Users.Update(newUserId), - new { UserName = newUserName, IsAdministrator = false, AllowCreateOrganization = true, UpdatePassword = false }, - TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); - - userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword); - resp = await userClient.PostAsJsonAsync( - Routes.Organizations.Create(), - new { Name = "test_org" }, - TestContext.Current.CancellationToken); - resp.EnsureSuccessStatusCode(); + await _testServer.Client.Users.PostAsync( + new CreateUserEndpointRequest { UserName = newUserName, Password = newUserPassword }, + cancellationToken: TestContext.Current.CancellationToken); + + { + var userClient = await _testServer.CreateClientForUserAsync(newUserName, newUserPassword); + + await ExpectApiErrorAsync(() => userClient.Organizations.PostAsync( + new CreateOrganizationEndpointRequest { Name = "test_org" }, + cancellationToken: TestContext.Current.CancellationToken), HttpStatusCode.Forbidden); + } + + // Extra step is required to get the ID of the created user + var allUsers = await _testServer.Client.Users.GetAsync(cancellationToken: TestContext.Current.CancellationToken); + var newUserId = allUsers!.Single(x => x.UserName!.Equals(newUserName)).Id!.Value; + + await _testServer.Client.Users[newUserId].PutAsync(new UpdateUserEndpointRequest + { + UserName = newUserName, + IsAdministrator = false, + AllowCreateOrganization = true, + UpdatePassword = false + }, cancellationToken: TestContext.Current.CancellationToken); + + { + // We need to create a new client because a fresh login is required to get the new claims in the token + var userClient = await _testServer.CreateClientForUserAsync(newUserName, newUserPassword); + + await userClient.Organizations.PostAsync( + new CreateOrganizationEndpointRequest { Name = "test_org" }, + cancellationToken: TestContext.Current.CancellationToken); + } + } + + private static async Task ExpectApiErrorAsync(Func func, HttpStatusCode code) + { + ApiException? exception = null; + + try + { + await func(); + } + catch (ApiException ex) + { + exception = ex; + } + + exception.Should().NotBeNull(); + exception.ResponseStatusCode.Should().Be((int)code); } } diff --git a/src/Turnierplan.App.Test.Functional/TestServer.cs b/src/Turnierplan.App.Test.Functional/TestServer.cs index bf0ec2e8..bab42a07 100644 --- a/src/Turnierplan.App.Test.Functional/TestServer.cs +++ b/src/Turnierplan.App.Test.Functional/TestServer.cs @@ -1,9 +1,14 @@ -using System.Net.Http.Json; +using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Kiota.Abstractions.Authentication; +using Microsoft.Kiota.Http.HttpClientLibrary; +using Turnierplan.App.Test.Functional.Client; +using Turnierplan.App.Test.Functional.Client.Api; +using Turnierplan.App.Test.Functional.Client.Models; using Turnierplan.Core.User; using Turnierplan.Dal; @@ -42,25 +47,27 @@ public TestServer() ctx.SaveChanges(); } - Client = CreateNewClientAndLogIn(username, password); + Client = CreateClientForUserAsync(username, password).GetAwaiter().GetResult(); } - public HttpClient Client { get; } + public ApiRequestBuilder Client { get; } - public HttpClient CreateNewClientAndLogIn(string username, string password) + public async Task CreateClientForUserAsync(string username, string password) { - var loginRequest = new HttpRequestMessage(HttpMethod.Post, Routes.Identity.Login()) + var authenticationProvider = new AnonymousAuthenticationProvider(); + var httpClient = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); + var httpClientRequestAdapter = new HttpClientRequestAdapter(authenticationProvider, httpClient: httpClient); + var client = new TurnierplanClient(httpClientRequestAdapter); + + var loginResponse = await client.Api.Identity.Login.PostAsync(new LoginEndpointRequest { - Content = JsonContent.Create(new { UserName = username, Password = password}) - }; + UserName = username, + Password = password + }); - var client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true }); - var loginResponseTask = client.SendAsync(loginRequest); - loginResponseTask.Wait(); - var loginResponse = loginResponseTask.Result; - loginResponse.EnsureSuccessStatusCode(); + loginResponse!.Success.Should().BeTrue(); - return client; + return client.Api; } public void ExecuteContextAction(Action action) diff --git a/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj index fe0ea8e1..9dca8dd3 100644 --- a/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj +++ b/src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj @@ -11,9 +11,20 @@ + + + + + + + + + + + diff --git a/src/Turnierplan.Core/PublicId/PublicId.cs b/src/Turnierplan.Core/PublicId/PublicId.cs index 25d8f38f..400d9e52 100644 --- a/src/Turnierplan.Core/PublicId/PublicId.cs +++ b/src/Turnierplan.Core/PublicId/PublicId.cs @@ -143,4 +143,6 @@ private static ulong ConvertFromBytes(byte[] bytes) } public static implicit operator PublicId(ulong value) => new(value); + + public static implicit operator string(PublicId value) => value.ToString(); }