diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index 66066086..368af883 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -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 }}" diff --git a/Aquifer.sln b/Aquifer.sln index 718eb2a1..e9139377 100644 --- a/Aquifer.sln +++ b/Aquifer.sln @@ -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 @@ -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 @@ -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} diff --git a/Directory.Packages.props b/Directory.Packages.props index 5e9d3ba6..fd0437d6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,7 @@ + diff --git a/src/Aquifer.API/Endpoints/BibleBooks/List/Endpoint.cs b/src/Aquifer.API/Endpoints/BibleBooks/List/Endpoint.cs index a8af7b92..260578e6 100644 --- a/src/Aquifer.API/Endpoints/BibleBooks/List/Endpoint.cs +++ b/src/Aquifer.API/Endpoints/BibleBooks/List/Endpoint.cs @@ -4,7 +4,7 @@ namespace Aquifer.API.Endpoints.BibleBooks.List; -public class Endpoint : EndpointWithoutRequest> +public class Endpoint : EndpointWithoutRequest> { public override void Configure() { @@ -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); } } \ No newline at end of file diff --git a/src/Aquifer.API/Program.cs b/src/Aquifer.API/Program.cs index 2f07d1e2..2ea3690c 100644 --- a/src/Aquifer.API/Program.cs +++ b/src/Aquifer.API/Program.cs @@ -83,4 +83,7 @@ app.UseResponseCachingVaryByAllQueryKeys(); app.MapEndpoints(); -app.Run(); \ No newline at end of file +app.Run(); + +// make this class public in order to access from integration tests +public partial class Program; \ No newline at end of file diff --git a/tests/Aquifer.API.IntegrationTests/App.cs b/tests/Aquifer.API.IntegrationTests/App.cs new file mode 100644 index 00000000..810aabf2 --- /dev/null +++ b/tests/Aquifer.API.IntegrationTests/App.cs @@ -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; + +/// +/// This is a FastEndpoints that sits on top of a +/// 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 +/// +public sealed class App : AppFixture +{ + /// + /// The app is configured in before this method is called. + /// Only use this method to override or extend existing host configuration. + /// + 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); + } + } + + /// + /// The service registrations in run before this method is called. + /// Only use this method to add additional services or to override existing registrations from the API. + /// + protected override void ConfigureServices(IServiceCollection services) + { + } + + /// + /// 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. + /// + /// + /// Example of adding a default header to the existing client: + /// + /// + /// Client.DefaultRequestHeaders.Add("api-key", "TODO"); + /// + /// + /// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client). + /// + protected override Task SetupAsync() + { + return Task.CompletedTask; + } + + /// + /// Use this method to dispose of any s created in . + /// + protected override Task TearDownAsync() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/Aquifer.API.IntegrationTests/Aquifer.API.IntegrationTests.csproj b/tests/Aquifer.API.IntegrationTests/Aquifer.API.IntegrationTests.csproj new file mode 100644 index 00000000..77be6022 --- /dev/null +++ b/tests/Aquifer.API.IntegrationTests/Aquifer.API.IntegrationTests.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Aquifer.API.IntegrationTests/Endpoints/Bibles/Books/List/EndpointTests.cs b/tests/Aquifer.API.IntegrationTests/Endpoints/Bibles/Books/List/EndpointTests.cs new file mode 100644 index 00000000..37a909aa --- /dev/null +++ b/tests/Aquifer.API.IntegrationTests/Endpoints/Bibles/Books/List/EndpointTests.cs @@ -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 +{ + [Fact] + public async Task ValidRequest_ShouldReturnSuccess() + { + var (response, result) = await _app.Client.GETAsync>(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + result.Should().HaveCountGreaterThan(80); + result.Should().Contain(r => r.Code == "4MA"); + } +} \ No newline at end of file diff --git a/tests/Aquifer.API.IntegrationTests/Endpoints/Resources/Content/Get/EndpointTests.cs b/tests/Aquifer.API.IntegrationTests/Endpoints/Resources/Content/Get/EndpointTests.cs new file mode 100644 index 00000000..575bb11d --- /dev/null +++ b/tests/Aquifer.API.IntegrationTests/Endpoints/Resources/Content/Get/EndpointTests.cs @@ -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 +{ + [Fact] + public async Task UnauthenticatedRequest_ShouldReturnUnauthorized() + { + var (response, _) = await _app.Client.GETAsync( + new Request + { + Id = 1890, + }); + + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + } +} \ No newline at end of file diff --git a/tests/Aquifer.Public.API.IntegrationTests/App.cs b/tests/Aquifer.Public.API.IntegrationTests/App.cs index 4f804b24..05b38cb6 100644 --- a/tests/Aquifer.Public.API.IntegrationTests/App.cs +++ b/tests/Aquifer.Public.API.IntegrationTests/App.cs @@ -15,26 +15,6 @@ namespace Aquifer.Public.API.IntegrationTests; /// public sealed class App : AppFixture { - /// - /// 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. - /// - /// - /// Example of adding a default header to the existing client: - /// - /// - /// Client.DefaultRequestHeaders.Add("api-key", "TODO"); - /// - /// - /// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client). - /// - protected override Task SetupAsync() - { - return Task.CompletedTask; - } - /// /// The app is configured in before this method is called. /// Only use this method to override or extend existing host configuration. @@ -57,6 +37,26 @@ protected override void ConfigureServices(IServiceCollection services) { } + /// + /// 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. + /// + /// + /// Example of adding a default header to the existing client: + /// + /// + /// Client.DefaultRequestHeaders.Add("api-key", "TODO"); + /// + /// + /// An entirely new client could be defined in this class and set up in this method as well (e.g. an authenticated client). + /// + protected override Task SetupAsync() + { + return Task.CompletedTask; + } + /// /// Use this method to dispose of any s created in . /// diff --git a/tests/Aquifer.Public.API.IntegrationTests/Endpoints/Bibles/List/EndpointTests.cs b/tests/Aquifer.Public.API.IntegrationTests/Endpoints/Bibles/List/EndpointTests.cs index 7e1402a7..e318644d 100644 --- a/tests/Aquifer.Public.API.IntegrationTests/Endpoints/Bibles/List/EndpointTests.cs +++ b/tests/Aquifer.Public.API.IntegrationTests/Endpoints/Bibles/List/EndpointTests.cs @@ -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"); } } \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 117cf812..34801561 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -9,12 +9,14 @@ + +