Skip to content

Commit 4811198

Browse files
Add test with TestServer
Add a test for the API that uses TestServer and have the HttpServerFixture derive from that.
1 parent 7a2d89f commit 4811198

File tree

6 files changed

+241
-94
lines changed

6 files changed

+241
-94
lines changed

tests/TodoApp.Tests/ApiTests.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright (c) Martin Costello, 2021. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Net.Http.Json;
7+
using System.Text.Json;
8+
using Microsoft.AspNetCore.Mvc.Testing;
9+
using TodoApp.Models;
10+
11+
namespace TodoApp
12+
{
13+
[Collection(TodoAppCollection.Name)]
14+
public class ApiTests
15+
{
16+
public ApiTests(TodoAppFixture fixture, ITestOutputHelper outputHelper)
17+
{
18+
Fixture = fixture;
19+
OutputHelper = outputHelper;
20+
Fixture.SetOutputHelper(OutputHelper);
21+
}
22+
23+
private TodoAppFixture Fixture { get; }
24+
25+
private ITestOutputHelper OutputHelper { get; }
26+
27+
[Fact]
28+
public async Task Can_Manage_Todo_Items_With_Api()
29+
{
30+
// Arrange
31+
var client = await CreateAuthenticatedClientAsync();
32+
33+
// Act - Get all the items
34+
var items = await client.GetFromJsonAsync<TodoListViewModel>("/api/items");
35+
36+
// Assert - There should be no items
37+
items.ShouldNotBeNull();
38+
items.Items.ShouldNotBeNull();
39+
40+
var beforeCount = items.Items.Count;
41+
42+
// Arrange
43+
var text = "Buy eggs";
44+
var newItem = new CreateTodoItemModel { Text = text };
45+
46+
// Act - Add a new item
47+
using var createdResponse = await client.PostAsJsonAsync("/api/items", newItem);
48+
49+
// Assert - An item was created
50+
createdResponse.StatusCode.ShouldBe(HttpStatusCode.Created);
51+
createdResponse.Headers.Location.ShouldNotBeNull();
52+
53+
using var createdStream = await createdResponse.Content.ReadAsStreamAsync();
54+
using var createdJson = await JsonDocument.ParseAsync(createdStream);
55+
56+
// Arrange - Get the new item's URL and Id
57+
var itemUri = createdResponse.Headers.Location;
58+
var itemId = createdJson.RootElement.GetProperty("id").GetString();
59+
60+
// Act - Get the item
61+
var item = await client.GetFromJsonAsync<TodoItemModel>(itemUri);
62+
63+
// Assert - Validate the item was created correctly
64+
item.ShouldNotBeNull();
65+
item.Id.ShouldBe(itemId);
66+
item.IsCompleted.ShouldBeFalse();
67+
item.LastUpdated.ShouldNotBeNull();
68+
item.Text.ShouldBe(text);
69+
70+
// Act - Mark the item as being completed
71+
using var completedResponse = await client.PostAsJsonAsync(itemUri + "/complete", new { });
72+
73+
// Assert - The item was completed
74+
completedResponse.StatusCode.ShouldBe(HttpStatusCode.NoContent);
75+
76+
item = await client.GetFromJsonAsync<TodoItemModel>(itemUri);
77+
78+
item.ShouldNotBeNull();
79+
item.Id.ShouldBe(itemId);
80+
item.Text.ShouldBe(text);
81+
item.IsCompleted.ShouldBeTrue();
82+
83+
// Act - Get all the items
84+
items = await client.GetFromJsonAsync<TodoListViewModel>("/api/items");
85+
86+
// Assert - The item was completed
87+
items.ShouldNotBeNull();
88+
items.Items.ShouldNotBeNull();
89+
items.Items.Count.ShouldBe(beforeCount + 1);
90+
item = items.Items.Last();
91+
92+
item.ShouldNotBeNull();
93+
item.Id.ShouldBe(itemId);
94+
item.Text.ShouldBe(text);
95+
item.IsCompleted.ShouldBeTrue();
96+
item.LastUpdated.ShouldNotBeNull();
97+
98+
// Act - Delete the item
99+
using var deletedResponse = await client.DeleteAsync(itemUri);
100+
101+
// Assert - The item no longer exists
102+
deletedResponse.StatusCode.ShouldBe(HttpStatusCode.NoContent);
103+
104+
items = await client.GetFromJsonAsync<TodoListViewModel>("/api/items");
105+
106+
items.ShouldNotBeNull();
107+
items.Items.ShouldNotBeNull();
108+
items.Items.Count.ShouldBe(beforeCount);
109+
items.Items.ShouldNotContain(p => p.Id == itemId);
110+
111+
var exception = await Assert.ThrowsAsync<HttpRequestException>(
112+
() => client.GetFromJsonAsync<TodoItemModel>(itemUri));
113+
114+
exception.StatusCode.ShouldBe(HttpStatusCode.NotFound);
115+
}
116+
117+
private async Task<HttpClient> CreateAuthenticatedClientAsync()
118+
{
119+
var options = new WebApplicationFactoryClientOptions();
120+
var client = Fixture.CreateClient(options);
121+
122+
var parameters = Array.Empty<KeyValuePair<string?, string?>>();
123+
using var content = new FormUrlEncodedContent(parameters);
124+
125+
// Go through the sign-in flow, which will set
126+
// the authentication cookie on the HttpClient.
127+
using var response = await client.PostAsync("/signin", content);
128+
129+
response.IsSuccessStatusCode.ShouldBeTrue();
130+
131+
return client;
132+
}
133+
}
134+
}

tests/TodoApp.Tests/HttpServerCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ namespace TodoApp
66
[CollectionDefinition(Name)]
77
public sealed class HttpServerCollection : ICollectionFixture<HttpServerFixture>
88
{
9-
public const string Name = "HTTP server collection";
9+
public const string Name = "TodoApp HTTP server collection";
1010
}
1111
}
Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,24 @@
11
// Copyright (c) Martin Costello, 2021. All rights reserved.
22
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
33

4-
using System.IO;
54
using System.Net.Http;
6-
using System.Reflection;
75
using System.Security.Cryptography.X509Certificates;
8-
using AspNet.Security.OAuth.GitHub;
9-
using JustEat.HttpClientInterception;
10-
using MartinCostello.Logging.XUnit;
116
using Microsoft.AspNetCore.Hosting;
127
using Microsoft.AspNetCore.Hosting.Server;
138
using Microsoft.AspNetCore.Hosting.Server.Features;
14-
using Microsoft.AspNetCore.Mvc.Testing;
15-
using Microsoft.Extensions.Configuration;
169
using Microsoft.Extensions.DependencyInjection;
1710
using Microsoft.Extensions.Hosting;
18-
using Microsoft.Extensions.Http;
19-
using Microsoft.Extensions.Logging;
20-
using Microsoft.Extensions.Options;
2111

2212
namespace TodoApp
2313
{
24-
public sealed class HttpServerFixture : WebApplicationFactory<Startup>, IAsyncLifetime, ITestOutputHelperAccessor
14+
public sealed class HttpServerFixture : TodoAppFixture, IAsyncLifetime
2515
{
2616
private IHost? _host;
2717
private bool _disposed;
2818

29-
public HttpServerFixture()
30-
: base()
31-
{
32-
ClientOptions.AllowAutoRedirect = false;
33-
ClientOptions.BaseAddress = new Uri("https://localhost");
34-
Interceptor = new HttpClientInterceptorOptions().ThrowsOnMissingRegistration();
35-
}
36-
37-
public HttpClientInterceptorOptions Interceptor { get; }
38-
39-
public ITestOutputHelper? OutputHelper { get; set; }
40-
4119
public string ServerAddress => ClientOptions.BaseAddress.ToString();
4220

43-
public override IServiceProvider? Services => _host?.Services;
44-
45-
public void ClearOutputHelper()
46-
=> OutputHelper = null;
47-
48-
public void SetOutputHelper(ITestOutputHelper value)
49-
=> OutputHelper = value;
21+
public override IServiceProvider Services => _host?.Services!;
5022

5123
async Task IAsyncLifetime.InitializeAsync()
5224
=> await EnsureHttpServerAsync();
@@ -92,49 +64,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
9264
{
9365
base.ConfigureWebHost(builder);
9466

95-
builder.ConfigureAppConfiguration((builder) =>
96-
{
97-
string dataDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
98-
99-
if (!Directory.Exists(dataDirectory))
100-
{
101-
Directory.CreateDirectory(dataDirectory);
102-
}
103-
104-
var config = new[]
105-
{
106-
KeyValuePair.Create("DataDirectory", dataDirectory),
107-
};
108-
109-
string? directory = Path.GetDirectoryName(typeof(HttpServerFixture).Assembly.Location);
110-
string fullPath = Path.Combine(directory ?? ".", "testsettings.json");
111-
112-
builder.Sources.Clear();
113-
114-
builder.AddJsonFile(fullPath)
115-
.AddInMemoryCollection(config);
116-
});
117-
11867
builder.ConfigureKestrel(
119-
(serverOptions) => serverOptions.ConfigureHttpsDefaults(
120-
(httpsOptions) => httpsOptions.ServerCertificate = new X509Certificate2("localhost-dev.pfx", "Pa55w0rd!")));
121-
122-
builder.ConfigureLogging((loggingBuilder) => loggingBuilder.ClearProviders().AddXUnit(this))
123-
.UseContentRoot(GetApplicationContentRootPath());
124-
125-
builder.ConfigureServices((services) =>
126-
{
127-
services.AddSingleton<IHttpMessageHandlerBuilderFilter, HttpRequestInterceptionFilter>(
128-
(_) => new HttpRequestInterceptionFilter(Interceptor));
129-
130-
services.AddSingleton<IPostConfigureOptions<GitHubAuthenticationOptions>, RemoteAuthorizationEventsFilter>();
131-
});
68+
serverOptions => serverOptions.ConfigureHttpsDefaults(
69+
httpsOptions => httpsOptions.ServerCertificate = new X509Certificate2("localhost-dev.pfx", "Pa55w0rd!")));
13270

13371
// Configure the server address for the server to
13472
// listen on for HTTPS requests on a dynamic port.
13573
builder.UseUrls("https://127.0.0.1:0");
136-
137-
Interceptor.RegisterBundle("oauth-http-bundle.json");
13874
}
13975

14076
protected override void Dispose(bool disposing)
@@ -154,38 +90,28 @@ protected override void Dispose(bool disposing)
15490

15591
private async Task EnsureHttpServerAsync()
15692
{
157-
if (_host == null)
93+
if (_host is null)
15894
{
159-
await CreateHttpServer();
95+
await CreateHttpServerAsync();
16096
}
16197
}
16298

163-
private async Task CreateHttpServer()
99+
private async Task CreateHttpServerAsync()
164100
{
165-
var builder = CreateHostBuilder().ConfigureWebHost(ConfigureWebHost);
166-
167-
_host = builder.Build();
101+
_host = CreateHostBuilder()!
102+
.ConfigureWebHost(ConfigureWebHost)
103+
.Build();
168104

169105
// Force creation of the Kestrel server and start it
170106
var hostedService = _host.Services.GetService<IHostedService>();
171107
await hostedService!.StartAsync(default);
172108

173109
var server = _host.Services.GetRequiredService<IServer>();
110+
var addresses = server.Features.Get<IServerAddressesFeature>();
174111

175-
ClientOptions.BaseAddress = server.Features.Get<IServerAddressesFeature>()!.Addresses
112+
ClientOptions.BaseAddress = addresses!.Addresses
176113
.Select((p) => new Uri(p))
177-
.First();
178-
}
179-
180-
private string GetApplicationContentRootPath()
181-
{
182-
var attribute = GetTestAssemblies()
183-
.SelectMany((p) => p.GetCustomAttributes<WebApplicationFactoryContentRootAttribute>())
184-
.Where((p) => string.Equals(p.Key, "TodoApp", StringComparison.OrdinalIgnoreCase))
185-
.OrderBy((p) => p.Priority)
186-
.First();
187-
188-
return attribute.ContentRootPath;
114+
.Last();
189115
}
190116
}
191117
}

tests/TodoApp.Tests/TodoApp.Tests.csproj

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@
2222
</ItemGroup>
2323
<ItemGroup>
2424
<ProjectReference Include="..\..\src\TodoApp\TodoApp.csproj" />
25-
<AssemblyAttribute Include="Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryContentRootAttribute">
26-
<_Parameter1>TodoApp</_Parameter1>
27-
<_Parameter2>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../src/TodoApp'))</_Parameter2>
28-
<_Parameter3>TodoApp.csproj</_Parameter3>
29-
<_Parameter4>-1</_Parameter4>
30-
</AssemblyAttribute>
3125
</ItemGroup>
3226
<PropertyGroup>
3327
<_PlaywrightCLIInstalledCommand Condition=" $([MSBuild]::IsOsPlatform('Windows')) ">where playwright</_PlaywrightCLIInstalledCommand>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Martin Costello, 2021. All rights reserved.
2+
// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information.
3+
4+
namespace TodoApp
5+
{
6+
[CollectionDefinition(Name)]
7+
public sealed class TodoAppCollection : ICollectionFixture<TodoAppFixture>
8+
{
9+
public const string Name = "TodoApp server collection";
10+
}
11+
}

0 commit comments

Comments
 (0)