Skip to content

Commit

Permalink
Add integration tests to Internal API. (#618)
Browse files Browse the repository at this point in the history
I didn't get time to get auth working but we can still add least add
tests for the anonymous routes and do auth'd routes later in a separate
PR.

I also tried out [FluentAssertions](https://fluentassertions.com/) which
I'm liking a lot so I added it to the test project global usings.
  • Loading branch information
NateMerritt authored Dec 20, 2024
1 parent 834cfdb commit 02f4fe6
Show file tree
Hide file tree
Showing 12 changed files with 172 additions and 26 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/pre-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ jobs:
dotnet test \
-e ASPNETCORE_ENVIRONMENT="${{ vars.ASPNETCORE_ENVIRONMENT }}" \
-e ConnectionStrings__AzureStorageAccount="${{ secrets.AZURE_STORAGE_ACCOUNT_CONNECTION_STRING }}" \
-e ConnectionStrings__BiblioNexusDb="${{ secrets.BIBLIONEXUS_DB_CONNECTION_STRING }}"
-e ConnectionStrings__BiblioNexusDb="${{ secrets.BIBLIONEXUS_DB_CONNECTION_STRING }}" \
-e KeyVaultUri="${{ secrets.KEY_VAULT_URI }}" \
-e JwtSettings__Authority="${{ secrets.JWT_SETTINGS_AUTHORITY }}" \
-e JwtSettings__Audience="${{ secrets.JWT_SETTINGS_AUDIENCE }}" \
-e Auth0Settings__ApiClientId="${{ secrets.AUTH0SETTINGS_API_CLIENT_ID }}" \
-e Auth0Settings__ApplicationClientId="${{ secrets.AUTH0SETTINGS_APPLICATION_CLIENT_ID }}" \
-e Auth0Settings__Audience="${{ secrets.AUTH0_SETTINGS_AUDIENCE }}" \
-e Auth0Settings__BaseUri="${{ secrets.AUTH0_SETTINGS_BASE_URI }}"
7 changes: 7 additions & 0 deletions Aquifer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquifer.Public.API.Integrat
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquifer.Jobs.UnitTests", "tests\Aquifer.Jobs.UnitTests\Aquifer.Jobs.UnitTests.csproj", "{5205B15E-7521-40A7-8C66-19C231839E4F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aquifer.API.IntegrationTests", "tests\Aquifer.API.IntegrationTests\Aquifer.API.IntegrationTests.csproj", "{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -118,6 +120,10 @@ Global
{5205B15E-7521-40A7-8C66-19C231839E4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5205B15E-7521-40A7-8C66-19C231839E4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5205B15E-7521-40A7-8C66-19C231839E4F}.Release|Any CPU.Build.0 = Release|Any CPU
{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -138,6 +144,7 @@ Global
{2F91DE0E-17AB-4DDD-9D73-9BE6CC1B1355} = {795E7203-D40F-4962-837D-DD39BAF2C930}
{0CF81CD4-6257-4A36-BF67-C1C581E93951} = {51DD9DE3-DA20-4C0B-82D6-029445F6BD42}
{5205B15E-7521-40A7-8C66-19C231839E4F} = {51DD9DE3-DA20-4C0B-82D6-029445F6BD42}
{EFCB9F65-BC0A-4AB6-9A42-19A0BD15ED30} = {51DD9DE3-DA20-4C0B-82D6-029445F6BD42}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8E32492B-EA66-40E4-A7A8-67E4ABA14A53}
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageVersion Include="FastEndpoints.ClientGen.Kiota" Version="5.32.0" />
<PackageVersion Include="FastEndpoints.Swagger" Version="5.32.0" />
<PackageVersion Include="FastEndpoints.Testing" Version="5.32.0" />
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.71" />
<PackageVersion Include="JavaScriptEngineSwitcher.V8" Version="3.24.2" />
<PackageVersion Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
Expand Down
6 changes: 4 additions & 2 deletions src/Aquifer.API/Endpoints/BibleBooks/List/Endpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Aquifer.API.Endpoints.BibleBooks.List;

public class Endpoint : EndpointWithoutRequest<IEnumerable<Response>>
public class Endpoint : EndpointWithoutRequest<IReadOnlyList<Response>>
{
public override void Configure()
{
Expand All @@ -21,7 +21,9 @@ public override async Task HandleAsync(CancellationToken ct)
Id = (int)x.BookId,
Name = x.BookFullName,
Code = x.BookCode
});
})
.ToList();

await SendOkAsync(response, ct);
}
}
5 changes: 4 additions & 1 deletion src/Aquifer.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,7 @@
app.UseResponseCachingVaryByAllQueryKeys();

app.MapEndpoints();
app.Run();
app.Run();

// make this class public in order to access from integration tests
public partial class Program;
68 changes: 68 additions & 0 deletions tests/Aquifer.API.IntegrationTests/App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using FastEndpoints.Testing;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Aquifer.API.IntegrationTests;

/// <summary>
/// This is a FastEndpoints <see cref="App"/> that sits on top of a <see cref="WebApplicationFactory{TEntryPoint}"/>
/// which allows for in-memory web requests to be sent and received without actual network traffic.
/// Details:
/// * https://fast-endpoints.com/docs/integration-unit-testing
/// * https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests
/// </summary>
public sealed class App : AppFixture<Program>
{
/// <summary>
/// The app is configured in <see cref="Program"/> before this method is called.
/// Only use this method to override or extend existing host configuration.
/// </summary>
protected override void ConfigureApp(IWebHostBuilder builder)
{
// The environment must be explicitly set as it will not default to "Development".
// On the build server this environment variable should be explicitly populated.
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")))
{
builder.UseEnvironment(Environments.Development);
}
}

/// <summary>
/// The service registrations in <see cref="Program"/> run before this method is called.
/// Only use this method to add additional services or to override existing registrations from the API.
/// </summary>
protected override void ConfigureServices(IServiceCollection services)
{
}

/// <summary>
/// Configure Clients here.
/// The default Client is anonymous and has no API Key header value.
/// This is sufficient for anonymous Internal API tests because no actual web requests are sent via this fixture.
/// The API key is enforced by Azure API Management when proxying requests, and thus it is not needed locally.
/// Various authenticated client with different roles/permissions are also needed.
/// </summary>
/// <remarks>
/// Example of adding a default header to the existing client:
/// <example>
/// <code>
/// Client.DefaultRequestHeaders.Add("api-key", "TODO");
/// </code>
/// </example>
/// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client).
/// </remarks>
protected override Task SetupAsync()
{
return Task.CompletedTask;
}

/// <summary>
/// Use this method to dispose of any <see cref="HttpClient"/>s created in <see cref="SetupAsync"/>.
/// </summary>
protected override Task TearDownAsync()
{
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FastEndpoints.Testing" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Aquifer.API\Aquifer.API.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using FastEndpoints;
using System.Net;
using Aquifer.API.Endpoints.BibleBooks.List;
using FastEndpoints.Testing;

namespace Aquifer.API.IntegrationTests.Endpoints.Bibles.Books.List;

public sealed class EndpointTests(App _app) : TestBase<App>
{
[Fact]
public async Task ValidRequest_ShouldReturnSuccess()
{
var (response, result) = await _app.Client.GETAsync<Endpoint, IReadOnlyList<Response>>();

response.StatusCode.Should().Be(HttpStatusCode.OK);
result.Should().HaveCountGreaterThan(80);
result.Should().Contain(r => r.Code == "4MA");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Net;
using Aquifer.API.Endpoints.Resources.Content.Get;
using FastEndpoints;
using FastEndpoints.Testing;

namespace Aquifer.API.IntegrationTests.Endpoints.Resources.Content.Get;

public sealed class EndpointTests(App _app) : TestBase<App>
{
[Fact]
public async Task UnauthenticatedRequest_ShouldReturnUnauthorized()
{
var (response, _) = await _app.Client.GETAsync<Endpoint, Request, ErrorResponse>(
new Request
{
Id = 1890,
});

response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
}
40 changes: 20 additions & 20 deletions tests/Aquifer.Public.API.IntegrationTests/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,6 @@ namespace Aquifer.Public.API.IntegrationTests;
/// </summary>
public sealed class App : AppFixture<Program>
{
/// <summary>
/// Configure Clients here.
/// The default Client is anonymous and has no API Key header value.
/// This is sufficient for Public API tests because all routes are anonymous and no actual web requests are sent via this fixture.
/// The API key is enforced by Azure API Management when proxying requests, and thus it is not needed locally.
/// </summary>
/// <remarks>
/// Example of adding a default header to the existing client:
/// <example>
/// <code>
/// Client.DefaultRequestHeaders.Add("api-key", "TODO");
/// </code>
/// </example>
/// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client).
/// </remarks>
protected override Task SetupAsync()
{
return Task.CompletedTask;
}

/// <summary>
/// The app is configured in <see cref="Program"/> before this method is called.
/// Only use this method to override or extend existing host configuration.
Expand All @@ -57,6 +37,26 @@ protected override void ConfigureServices(IServiceCollection services)
{
}

/// <summary>
/// Configure Clients here.
/// The default Client is anonymous and has no API Key header value.
/// This is sufficient for Public API tests because all routes are anonymous and no actual web requests are sent via this fixture.
/// The API key is enforced by Azure API Management when proxying requests, and thus it is not needed locally.
/// </summary>
/// <remarks>
/// Example of adding a default header to the existing client:
/// <example>
/// <code>
/// Client.DefaultRequestHeaders.Add("api-key", "TODO");
/// </code>
/// </example>
/// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client).
/// </remarks>
protected override Task SetupAsync()
{
return Task.CompletedTask;
}

/// <summary>
/// Use this method to dispose of any <see cref="HttpClient"/>s created in <see cref="SetupAsync"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public async Task ValidRequest_ShouldReturnSuccess()
HasGreekAlignment = true,
});

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(result, r => r.Abbreviation == "BSB");
response.StatusCode.Should().Be(HttpStatusCode.OK);

result.Should().Contain(r => r.Abbreviation == "BSB");
}
}
2 changes: 2 additions & 0 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>

<ItemGroup>
<Using Include="FluentAssertions" />
<Using Include="Xunit" />
</ItemGroup>

Expand Down

0 comments on commit 02f4fe6

Please sign in to comment.