Skip to content

Commit 15d2449

Browse files
authored
Use Kiota to generate HTTP client for functional tests (#364)
1 parent 7d066f8 commit 15d2449

7 files changed

Lines changed: 103 additions & 80 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"microsoft.openapi.kiota": {
6+
"version": "1.30.0",
7+
"commands": [
8+
"kiota"
9+
],
10+
"rollForward": false
11+
}
12+
}
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Generated HTTP clients
2+
Client

src/Turnierplan.App.Test.Functional/Routes.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
using System.Net;
2-
using System.Net.Http.Json;
32
using FluentAssertions;
43
using FluentAssertions.Extensions;
5-
using Turnierplan.App.Models;
4+
using Microsoft.Kiota.Abstractions;
5+
using Turnierplan.App.Test.Functional.Client.Models;
66
using Turnierplan.Core.ApiKey;
77
using Turnierplan.Core.Extensions;
88
using Turnierplan.Core.Organization;
9-
using Turnierplan.Core.RoleAssignment;
109
using Turnierplan.Core.Tournament;
1110
using Turnierplan.Core.User;
1211
using Xunit;
12+
using Role = Turnierplan.Core.RoleAssignment.Role;
13+
using Visibility = Turnierplan.Core.Tournament.Visibility;
1314

1415
namespace Turnierplan.App.Test.Functional;
1516

@@ -46,14 +47,12 @@ public async Task When_ApiKey_And_User_Are_Deleted_The_Role_Assignments_Are_Also
4647
_testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1);
4748
_testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(2);
4849

49-
var resp = await _testServer.Client.DeleteAsync(Routes.ApiKeys.Delete(apiKeyId), TestContext.Current.CancellationToken);
50-
resp.EnsureSuccessStatusCode();
50+
await _testServer.Client.ApiKeys[apiKeyId].DeleteAsync(cancellationToken: TestContext.Current.CancellationToken);
5151

5252
_testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(1);
5353
_testServer.ExecuteContextAction(db => db.TournamentRoleAssignments.Count()).Should().Be(1);
5454

55-
resp = await _testServer.Client.DeleteAsync(Routes.Users.Delete(userId), TestContext.Current.CancellationToken);
56-
resp.EnsureSuccessStatusCode();
55+
await _testServer.Client.Users[userId].DeleteAsync(cancellationToken: TestContext.Current.CancellationToken);
5756

5857
_testServer.ExecuteContextAction(db => db.OrganizationRoleAssignments.Count()).Should().Be(0);
5958
_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
6564
const string newUserName = "test_user";
6665
const string newUserPassword = "test123";
6766

68-
var resp = await _testServer.Client.PostAsJsonAsync(
69-
Routes.Users.Create(),
70-
new { UserName = newUserName, Password = newUserPassword },
71-
TestContext.Current.CancellationToken);
72-
resp.EnsureSuccessStatusCode();
73-
74-
var userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword);
75-
resp = await userClient.PostAsJsonAsync(
76-
Routes.Organizations.Create(),
77-
new { Name = "test_org" },
78-
TestContext.Current.CancellationToken);
79-
resp.StatusCode.Should().Be(HttpStatusCode.Forbidden);
80-
81-
// extra step required to get ID of new user
82-
resp = await _testServer.Client.GetAsync(Routes.Users.List(), TestContext.Current.CancellationToken);
83-
resp.EnsureSuccessStatusCode();
84-
var allUsers = await resp.Content.ReadFromJsonAsync<UserDto[]>(TestContext.Current.CancellationToken);
85-
var newUserId = allUsers!.Single(x => x.UserName.Equals(newUserName)).Id;
86-
87-
resp = await _testServer.Client.PutAsJsonAsync(
88-
Routes.Users.Update(newUserId),
89-
new { UserName = newUserName, IsAdministrator = false, AllowCreateOrganization = true, UpdatePassword = false },
90-
TestContext.Current.CancellationToken);
91-
resp.EnsureSuccessStatusCode();
92-
93-
userClient = _testServer.CreateNewClientAndLogIn(newUserName, newUserPassword);
94-
resp = await userClient.PostAsJsonAsync(
95-
Routes.Organizations.Create(),
96-
new { Name = "test_org" },
97-
TestContext.Current.CancellationToken);
98-
resp.EnsureSuccessStatusCode();
67+
await _testServer.Client.Users.PostAsync(
68+
new CreateUserEndpointRequest { UserName = newUserName, Password = newUserPassword },
69+
cancellationToken: TestContext.Current.CancellationToken);
70+
71+
{
72+
var userClient = await _testServer.CreateClientForUserAsync(newUserName, newUserPassword);
73+
74+
await ExpectApiErrorAsync(() => userClient.Organizations.PostAsync(
75+
new CreateOrganizationEndpointRequest { Name = "test_org" },
76+
cancellationToken: TestContext.Current.CancellationToken), HttpStatusCode.Forbidden);
77+
}
78+
79+
// Extra step is required to get the ID of the created user
80+
var allUsers = await _testServer.Client.Users.GetAsync(cancellationToken: TestContext.Current.CancellationToken);
81+
var newUserId = allUsers!.Single(x => x.UserName!.Equals(newUserName)).Id!.Value;
82+
83+
await _testServer.Client.Users[newUserId].PutAsync(new UpdateUserEndpointRequest
84+
{
85+
UserName = newUserName,
86+
IsAdministrator = false,
87+
AllowCreateOrganization = true,
88+
UpdatePassword = false
89+
}, cancellationToken: TestContext.Current.CancellationToken);
90+
91+
{
92+
// We need to create a new client because a fresh login is required to get the new claims in the token
93+
var userClient = await _testServer.CreateClientForUserAsync(newUserName, newUserPassword);
94+
95+
await userClient.Organizations.PostAsync(
96+
new CreateOrganizationEndpointRequest { Name = "test_org" },
97+
cancellationToken: TestContext.Current.CancellationToken);
98+
}
99+
}
100+
101+
private static async Task ExpectApiErrorAsync(Func<Task> func, HttpStatusCode code)
102+
{
103+
ApiException? exception = null;
104+
105+
try
106+
{
107+
await func();
108+
}
109+
catch (ApiException ex)
110+
{
111+
exception = ex;
112+
}
113+
114+
exception.Should().NotBeNull();
115+
exception.ResponseStatusCode.Should().Be((int)code);
99116
}
100117
}

src/Turnierplan.App.Test.Functional/TestServer.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
using System.Net.Http.Json;
1+
using FluentAssertions;
22
using Microsoft.AspNetCore.Hosting;
33
using Microsoft.AspNetCore.Identity;
44
using Microsoft.AspNetCore.Mvc.Testing;
55
using Microsoft.Extensions.Configuration;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Kiota.Abstractions.Authentication;
8+
using Microsoft.Kiota.Http.HttpClientLibrary;
9+
using Turnierplan.App.Test.Functional.Client;
10+
using Turnierplan.App.Test.Functional.Client.Api;
11+
using Turnierplan.App.Test.Functional.Client.Models;
712
using Turnierplan.Core.User;
813
using Turnierplan.Dal;
914

@@ -42,25 +47,27 @@ public TestServer()
4247
ctx.SaveChanges();
4348
}
4449

45-
Client = CreateNewClientAndLogIn(username, password);
50+
Client = CreateClientForUserAsync(username, password).GetAwaiter().GetResult();
4651
}
4752

48-
public HttpClient Client { get; }
53+
public ApiRequestBuilder Client { get; }
4954

50-
public HttpClient CreateNewClientAndLogIn(string username, string password)
55+
public async Task<ApiRequestBuilder> CreateClientForUserAsync(string username, string password)
5156
{
52-
var loginRequest = new HttpRequestMessage(HttpMethod.Post, Routes.Identity.Login())
57+
var authenticationProvider = new AnonymousAuthenticationProvider();
58+
var httpClient = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true });
59+
var httpClientRequestAdapter = new HttpClientRequestAdapter(authenticationProvider, httpClient: httpClient);
60+
var client = new TurnierplanClient(httpClientRequestAdapter);
61+
62+
var loginResponse = await client.Api.Identity.Login.PostAsync(new LoginEndpointRequest
5363
{
54-
Content = JsonContent.Create(new { UserName = username, Password = password})
55-
};
64+
UserName = username,
65+
Password = password
66+
});
5667

57-
var client = _application.CreateClient(new WebApplicationFactoryClientOptions { HandleCookies = true });
58-
var loginResponseTask = client.SendAsync(loginRequest);
59-
loginResponseTask.Wait();
60-
var loginResponse = loginResponseTask.Result;
61-
loginResponse.EnsureSuccessStatusCode();
68+
loginResponse!.Success.Should().BeTrue();
6269

63-
return client;
70+
return client.Api;
6471
}
6572

6673
public void ExecuteContextAction(Action<TurnierplanContext> action)

src/Turnierplan.App.Test.Functional/Turnierplan.App.Test.Functional.csproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.2" />
14+
<PackageReference Include="Microsoft.Kiota.Bundle" Version="1.22.0" />
1415
</ItemGroup>
1516

1617
<ItemGroup>
1718
<ProjectReference Include="..\Turnierplan.App\Turnierplan.App.csproj" />
1819
</ItemGroup>
20+
21+
<Target Name="GenerateKiotaClient" BeforeTargets="BeforeCompile">
22+
<Exec Command="dotnet tool restore"/>
23+
<Exec Command="dotnet tool run kiota generate -l CSharp -c TurnierplanClient -n Turnierplan.App.Test.Functional.Client -d ../Turnierplan.App/obj/Turnierplan.App_turnierplan.json -o ./Client --log-level error --exclude-backward-compatible"/>
24+
<ItemGroup>
25+
<!-- Required for the generated files to be discovered on the first build. -->
26+
<Compile Remove="Client/**/*.cs" />
27+
<Compile Include="Client/**/*.cs" />
28+
</ItemGroup>
29+
</Target>
1930
</Project>

src/Turnierplan.Core/PublicId/PublicId.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,6 @@ private static ulong ConvertFromBytes(byte[] bytes)
143143
}
144144

145145
public static implicit operator PublicId(ulong value) => new(value);
146+
147+
public static implicit operator string(PublicId value) => value.ToString();
146148
}

0 commit comments

Comments
 (0)