Skip to content

Commit e8767a8

Browse files
authored
Merge pull request #11 from sheepla/devin/1777621141-feat-example-integration-tests
Add integration tests for Example project
2 parents 8ba9d31 + 36ff921 commit e8767a8

11 files changed

Lines changed: 538 additions & 0 deletions

AxisEndpoints.sln

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
1313
EndProject
1414
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AxisEndpoints.Extensions.CsvHelper", "src\AxisEndpoints.Extensions.CsvHelper\AxisEndpoints.Extensions.CsvHelper.csproj", "{13A4EADD-A715-4934-847E-EAF7043B05AE}"
1515
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
17+
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AxisEndpoints.Example.Tests", "tests\AxisEndpoints.Example.Tests\AxisEndpoints.Example.Tests.csproj", "{386C36D3-22BE-47D3-81BD-5E27ABAF0657}"
19+
EndProject
1620
Global
1721
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1822
Debug|Any CPU = Debug|Any CPU
@@ -71,11 +75,24 @@ Global
7175
{13A4EADD-A715-4934-847E-EAF7043B05AE}.Release|x64.Build.0 = Release|Any CPU
7276
{13A4EADD-A715-4934-847E-EAF7043B05AE}.Release|x86.ActiveCfg = Release|Any CPU
7377
{13A4EADD-A715-4934-847E-EAF7043B05AE}.Release|x86.Build.0 = Release|Any CPU
78+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|Any CPU.Build.0 = Debug|Any CPU
80+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|x64.ActiveCfg = Debug|Any CPU
81+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|x64.Build.0 = Debug|Any CPU
82+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|x86.ActiveCfg = Debug|Any CPU
83+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Debug|x86.Build.0 = Debug|Any CPU
84+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|Any CPU.ActiveCfg = Release|Any CPU
85+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|Any CPU.Build.0 = Release|Any CPU
86+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|x64.ActiveCfg = Release|Any CPU
87+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|x64.Build.0 = Release|Any CPU
88+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|x86.ActiveCfg = Release|Any CPU
89+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657}.Release|x86.Build.0 = Release|Any CPU
7490
EndGlobalSection
7591
GlobalSection(SolutionProperties) = preSolution
7692
HideSolutionNode = FALSE
7793
EndGlobalSection
7894
GlobalSection(NestedProjects) = preSolution
7995
{13A4EADD-A715-4934-847E-EAF7043B05AE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
96+
{386C36D3-22BE-47D3-81BD-5E27ABAF0657} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
8097
EndGlobalSection
8198
EndGlobal
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using AxisEndpoints.Example.Features.Admin.Stats;
4+
using FluentAssertions;
5+
6+
namespace AxisEndpoints.Example.Tests;
7+
8+
public class AdminStatsEndpointTests : IClassFixture<ExampleWebApplicationFactory>
9+
{
10+
private readonly HttpClient _client;
11+
12+
public AdminStatsEndpointTests(ExampleWebApplicationFactory factory)
13+
{
14+
_client = factory.Client;
15+
}
16+
17+
[Fact]
18+
public async Task GetStats_DefaultDateRange_Returns200()
19+
{
20+
var response = await _client.GetAsync("/admin/stats");
21+
22+
response.StatusCode.Should().Be(HttpStatusCode.OK);
23+
var body = await response.Content.ReadFromJsonAsync<StatsResponse>();
24+
body.Should().NotBeNull();
25+
body!.TotalUsers.Should().Be(42);
26+
body.NewUsersInPeriod.Should().Be(7);
27+
}
28+
29+
[Fact]
30+
public async Task GetStats_WithDateRange_Returns200WithCorrectDates()
31+
{
32+
var response = await _client.GetAsync("/admin/stats?from=2025-01-01T00:00:00Z&to=2025-12-31T00:00:00Z");
33+
34+
response.StatusCode.Should().Be(HttpStatusCode.OK);
35+
var body = await response.Content.ReadFromJsonAsync<StatsResponse>();
36+
body.Should().NotBeNull();
37+
body!.From.Should().Be(new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero));
38+
body.To.Should().Be(new DateTimeOffset(2025, 12, 31, 0, 0, 0, TimeSpan.Zero));
39+
}
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net10.0</TargetFramework>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<Nullable>enable</Nullable>
6+
<IsPackable>false</IsPackable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
11+
<PackageReference Include="FluentAssertions" Version="8.*" />
12+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.*" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<ProjectReference Include="..\..\src\AxisEndpoints\AxisEndpoints.csproj" />
20+
<ProjectReference Include="..\..\src\AxisEndpoints.Extensions.CsvHelper\AxisEndpoints.Extensions.CsvHelper.csproj" />
21+
<ProjectReference Include="..\AxisEndpoints.Example\AxisEndpoints.Example.csproj" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<Using Include="Xunit" />
26+
</ItemGroup>
27+
</Project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using AxisEndpoints.Example.Features.Users;
4+
using FluentAssertions;
5+
6+
namespace AxisEndpoints.Example.Tests;
7+
8+
public class CreateUserEndpointTests : IClassFixture<ExampleWebApplicationFactory>
9+
{
10+
private readonly HttpClient _client;
11+
12+
public CreateUserEndpointTests(ExampleWebApplicationFactory factory)
13+
{
14+
_client = factory.Client;
15+
}
16+
17+
[Fact]
18+
public async Task CreateUser_ValidRequest_Returns201WithLocationHeader()
19+
{
20+
var response = await _client.PostAsJsonAsync("/api/users", new
21+
{
22+
Name = "Eve",
23+
Email = "eve@example.com",
24+
Role = "User",
25+
});
26+
27+
response.StatusCode.Should().Be(HttpStatusCode.Created);
28+
response.Headers.Location.Should().NotBeNull();
29+
response.Headers.Location!.ToString().Should().Contain("/api/users/1");
30+
31+
var body = await response.Content.ReadFromJsonAsync<UserResponse>();
32+
body.Should().NotBeNull();
33+
body!.Name.Should().Be("Eve");
34+
body.Email.Should().Be("eve@example.com");
35+
body.Role.Should().Be("User");
36+
}
37+
38+
[Fact]
39+
public async Task CreateUser_DefaultRole_ReturnsUser()
40+
{
41+
var response = await _client.PostAsJsonAsync("/api/users", new
42+
{
43+
Name = "Frank",
44+
Email = "frank@example.com",
45+
});
46+
47+
response.StatusCode.Should().Be(HttpStatusCode.Created);
48+
var body = await response.Content.ReadFromJsonAsync<UserResponse>();
49+
body.Should().NotBeNull();
50+
body!.Role.Should().Be("User");
51+
}
52+
53+
[Fact]
54+
public async Task CreateUser_InvalidEmail_Returns400()
55+
{
56+
var response = await _client.PostAsJsonAsync("/api/users", new
57+
{
58+
Name = "Eve",
59+
Email = "not-an-email",
60+
});
61+
62+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
63+
}
64+
65+
[Fact]
66+
public async Task CreateUser_NameTooLong_Returns400()
67+
{
68+
var response = await _client.PostAsJsonAsync("/api/users", new
69+
{
70+
Name = new string('A', 101),
71+
Email = "toolong@example.com",
72+
});
73+
74+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
75+
}
76+
77+
[Fact]
78+
public async Task CreateUser_MissingName_Returns400()
79+
{
80+
var response = await _client.PostAsJsonAsync("/api/users", new
81+
{
82+
Email = "missing@example.com",
83+
});
84+
85+
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
86+
}
87+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Net;
2+
using System.Text;
3+
using AxisEndpoints.Extensions.CsvHelper;
4+
using FluentAssertions;
5+
6+
namespace AxisEndpoints.Example.Tests;
7+
8+
public class CsvEndpointTests : IClassFixture<ExampleWebApplicationFactory>
9+
{
10+
private readonly HttpClient _client;
11+
12+
public CsvEndpointTests(ExampleWebApplicationFactory factory)
13+
{
14+
_client = factory.Client;
15+
}
16+
17+
[Fact]
18+
public async Task ExportCsv_Returns200WithCsvContent()
19+
{
20+
var response = await _client.GetAsync("/api/users/users/export");
21+
22+
response.StatusCode.Should().Be(HttpStatusCode.OK);
23+
response.Content.Headers.ContentType!.MediaType.Should().Be("text/csv");
24+
25+
var csv = await response.Content.ReadAsStringAsync();
26+
csv.Should().Contain("id");
27+
csv.Should().Contain("name");
28+
csv.Should().Contain("Alice");
29+
csv.Should().Contain("Bob");
30+
}
31+
32+
[Fact]
33+
public async Task ExportCsv_HasContentDispositionHeader()
34+
{
35+
var response = await _client.GetAsync("/api/users/users/export");
36+
37+
response.Content.Headers.ContentDisposition.Should().NotBeNull();
38+
response.Content.Headers.ContentDisposition!.FileName.Should().Be("users.csv");
39+
}
40+
41+
[Fact]
42+
public async Task ImportCsv_ValidCsv_Returns204()
43+
{
44+
var csvContent = "name,email,role\nAlice,alice@example.com,Admin\nBob,bob@example.com,User";
45+
var content = new StringContent(csvContent, Encoding.UTF8, "text/csv");
46+
47+
var response = await _client.PostAsync("/api/users/users/import", content);
48+
49+
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
50+
}
51+
52+
/// <summary>
53+
/// CsvBindingException is thrown during BindAsync (parameter binding phase),
54+
/// which executes before the endpoint filter pipeline. As a result,
55+
/// CsvBindingExceptionFilter cannot catch it and the exception propagates
56+
/// as an unhandled server error.
57+
/// </summary>
58+
[Fact]
59+
public async Task ImportCsv_InvalidRow_ThrowsCsvBindingException()
60+
{
61+
var csvContent = "name,email,role\n,invalid-email,";
62+
var content = new StringContent(csvContent, Encoding.UTF8, "text/csv");
63+
64+
var act = () => _client.PostAsync("/api/users/users/import", content);
65+
66+
await act.Should()
67+
.ThrowAsync<Exception>()
68+
.Where(ex => ex.ToString().Contains(nameof(CsvBindingException)));
69+
}
70+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Net;
2+
using FluentAssertions;
3+
4+
namespace AxisEndpoints.Example.Tests;
5+
6+
public class DeleteUserEndpointTests : IClassFixture<ExampleWebApplicationFactory>
7+
{
8+
private readonly HttpClient _client;
9+
10+
public DeleteUserEndpointTests(ExampleWebApplicationFactory factory)
11+
{
12+
_client = factory.Client;
13+
}
14+
15+
[Fact]
16+
public async Task DeleteUser_Returns204NoContent()
17+
{
18+
var response = await _client.DeleteAsync("/api/users/1");
19+
20+
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
21+
}
22+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Reflection;
2+
using AxisEndpoints.Example.Features.Health;
3+
using AxisEndpoints.Extensions;
4+
using AxisEndpoints.Extensions.CsvHelper;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.TestHost;
7+
using Microsoft.Extensions.Hosting;
8+
9+
namespace AxisEndpoints.Example.Tests;
10+
11+
public class ExampleWebApplicationFactory : IAsyncLifetime, IDisposable
12+
{
13+
private WebApplication? _app;
14+
private HttpClient? _client;
15+
private static readonly Assembly ExampleAssembly = typeof(HealthEndpoint).Assembly;
16+
17+
public HttpClient Client =>
18+
_client ?? throw new InvalidOperationException("Test client is not initialized.");
19+
20+
public async Task InitializeAsync()
21+
{
22+
var builder = WebApplication.CreateBuilder();
23+
builder.WebHost.UseTestServer();
24+
25+
builder.Services.AddAxisEndpoints(ExampleAssembly);
26+
builder.Services.AddAxisEndpointsCsvHelper();
27+
28+
_app = builder.Build();
29+
_app.MapAxisEndpoints(ExampleAssembly);
30+
31+
await _app.StartAsync();
32+
_client = _app.GetTestClient();
33+
}
34+
35+
public async Task DisposeAsync()
36+
{
37+
_client?.Dispose();
38+
_client = null;
39+
if (_app is not null)
40+
{
41+
await _app.StopAsync();
42+
await _app.DisposeAsync();
43+
_app = null;
44+
}
45+
}
46+
47+
public void Dispose()
48+
{
49+
// IAsyncLifetime.DisposeAsync() is called by xUnit; this is a no-op fallback.
50+
}
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Net;
2+
using System.Net.Http.Json;
3+
using AxisEndpoints.Example.Features.Users;
4+
using FluentAssertions;
5+
6+
namespace AxisEndpoints.Example.Tests;
7+
8+
public class FindByIdEndpointTests : IClassFixture<ExampleWebApplicationFactory>
9+
{
10+
private readonly HttpClient _client;
11+
12+
public FindByIdEndpointTests(ExampleWebApplicationFactory factory)
13+
{
14+
_client = factory.Client;
15+
}
16+
17+
[Fact]
18+
public async Task FindById_ExistingUser_Returns200()
19+
{
20+
var response = await _client.GetAsync("/api/users/1");
21+
22+
response.StatusCode.Should().Be(HttpStatusCode.OK);
23+
var body = await response.Content.ReadFromJsonAsync<UserResponse>();
24+
body.Should().NotBeNull();
25+
body!.Id.Should().Be(1);
26+
body.Name.Should().Be("Alice");
27+
body.Email.Should().Be("alice@example.com");
28+
}
29+
30+
[Fact]
31+
public async Task FindById_NonExistingUser_Returns404()
32+
{
33+
var response = await _client.GetAsync("/api/users/999");
34+
35+
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
36+
}
37+
38+
[Fact]
39+
public async Task FindById_WithJapaneseLanguageHeader_ReturnsJapaneseName()
40+
{
41+
var request = new HttpRequestMessage(HttpMethod.Get, "/api/users/1");
42+
request.Headers.Add("Accept-Language", "ja");
43+
44+
var response = await _client.SendAsync(request);
45+
46+
response.StatusCode.Should().Be(HttpStatusCode.OK);
47+
var body = await response.Content.ReadFromJsonAsync<UserResponse>();
48+
body.Should().NotBeNull();
49+
body!.Name.Should().Contain("山田");
50+
}
51+
}

0 commit comments

Comments
 (0)