diff --git a/API.IntegrationTests/API.IntegrationTests.csproj b/API.IntegrationTests/API.IntegrationTests.csproj new file mode 100644 index 00000000..f097190c --- /dev/null +++ b/API.IntegrationTests/API.IntegrationTests.csproj @@ -0,0 +1,22 @@ + + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + diff --git a/API.IntegrationTests/AccountTests.cs b/API.IntegrationTests/AccountTests.cs new file mode 100644 index 00000000..0d0a9b08 --- /dev/null +++ b/API.IntegrationTests/AccountTests.cs @@ -0,0 +1,29 @@ +using System.Net; +using System.Text; +using System.Text.Json; + +namespace API.IntegrationTests; + +public class AccountTests : BaseIntegrationTest +{ + [Test] + public async Task CreateAccount_ShouldAdd_NewUserToDatabase() + { + using var client = WebAppFactory.CreateClient(); + + var requestBody = JsonSerializer.Serialize(new + { + username = "Bob", + password = "SecurePassword123#", + email = "bob@example.com", + turnstileresponse = "lmao" + }); + + + var response = await client.PostAsync("/2/account/signup", new StringContent(requestBody, Encoding.UTF8, "application/json")); + + var content = await response.Content.ReadAsStringAsync(); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + } +} diff --git a/API.IntegrationTests/BaseIntegrationTest.cs b/API.IntegrationTests/BaseIntegrationTest.cs new file mode 100644 index 00000000..babdbeef --- /dev/null +++ b/API.IntegrationTests/BaseIntegrationTest.cs @@ -0,0 +1,7 @@ +namespace API.IntegrationTests; + +public abstract class BaseIntegrationTest +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required IntegrationTestWebAppFactory WebAppFactory { get; init; } +} \ No newline at end of file diff --git a/API.IntegrationTests/IntegrationTestWebAppFactory.cs b/API.IntegrationTests/IntegrationTestWebAppFactory.cs new file mode 100644 index 00000000..e0d4bd5c --- /dev/null +++ b/API.IntegrationTests/IntegrationTestWebAppFactory.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Testcontainers.PostgreSql; +using Testcontainers.Redis; +using TUnit.Core.Interfaces; + +namespace API.IntegrationTests; + +public class IntegrationTestWebAppFactory : WebApplicationFactory, IAsyncInitializer +{ + private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder() + .WithImage("postgres:latest") + .WithDatabase("openshock") + .WithUsername("openshock") + .WithPassword("superSecurePassword") + .Build(); + + private readonly RedisContainer _redisContainer = new RedisBuilder() + .WithImage("redis/redis-stack-server:latest") + .Build(); + + + public async Task InitializeAsync() + { + await _dbContainer.StartAsync(); + await _redisContainer.StartAsync(); + } + + protected override IWebHostBuilder? CreateWebHostBuilder() + { + var environmentVariables = new Dictionary + { + { "ASPNETCORE_UNDER_INTEGRATION_TEST", "1" }, + + { "OPENSHOCK__DB__CONN", _dbContainer.GetConnectionString() }, + { "OPENSHOCK__DB__SKIPMIGRATION", "false" }, + { "OPENSHOCK__DB__DEBUG", "false" }, + + { "OPENSHOCK__REDIS__CONN", _redisContainer.GetConnectionString() }, + { "OPENSHOCK__REDIS__HOST", "" }, + { "OPENSHOCK__REDIS__USER", "" }, + { "OPENSHOCK__REDIS__PASSWORD", "" }, + { "OPENSHOCK__REDIS__PORT", "6379" }, + + { "OPENSHOCK__FRONTEND__BASEURL", "https://openshock.app" }, + { "OPENSHOCK__FRONTEND__SHORTURL", "https://openshock.app" }, + { "OPENSHOCK__FRONTEND__COOKIEDOMAIN", "openshock.app" }, + + { "OPENSHOCK__MAIL__TYPE", "MAILJET" }, + { "OPENSHOCK__MAIL__SENDER__EMAIL", "system@openshock.org" }, + { "OPENSHOCK__MAIL__SENDER__NAME", "OpenShock" }, + { "OPENSHOCK__MAIL__MAILJET__KEY", "mailjet-key" }, + { "OPENSHOCK__MAIL__MAILJET__SECRET", "mailjet-secret" }, + { "OPENSHOCK__MAIL__MAILJET__TEMPLATE__PASSWORDRESET", "12345678" }, + { "OPENSHOCK__MAIL__MAILJET__TEMPLATE__PASSWORDRESETCOMPLETE", "87654321" }, + { "OPENSHOCK__MAIL__MAILJET__TEMPLATE__VERIFYEMAIL", "11223344" }, + { "OPENSHOCK__MAIL__MAILJET__TEMPLATE__VERIFYEMAILCOMPLETE", "44332211" }, + + { "OPENSHOCK__TURNSTILE__ENABLED", "true" }, + { "OPENSHOCK__TURNSTILE__SECRETKEY", "turnstile-secret-key" }, + { "OPENSHOCK__TURNSTILE__SITEKEY", "turnstile-site-key" }, + + { "OPENSHOCK__LCG__FQDN", "de1-gateway.my-openshock-instance.net" }, + { "OPENSHOCK__LCG__COUNTRYCODE", "DE" } + }; + + foreach (var envVar in environmentVariables) + { + Environment.SetEnvironmentVariable(envVar.Key, envVar.Value); + } + + return base.CreateWebHostBuilder(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + + builder.ConfigureTestServices(services => + { + // We can replace services here + }); + } + + public override async ValueTask DisposeAsync() + { + await _dbContainer.DisposeAsync(); + await _redisContainer.DisposeAsync(); + await base.DisposeAsync(); + } +} diff --git a/API/API.csproj b/API/API.csproj index 9ccf86a5..033d5fcd 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -67,4 +67,8 @@ Always + + + + diff --git a/API/Program.cs b/API/Program.cs index e3b00ba6..9b413b6a 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -131,3 +131,6 @@ app.MapScalarApiReference(options => options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json"); app.Run(); + +// Expose Program class for integrationtests +public partial class Program; \ No newline at end of file diff --git a/Common/OpenShockApplication.cs b/Common/OpenShockApplication.cs index 523fc940..fa5dad10 100644 --- a/Common/OpenShockApplication.cs +++ b/Common/OpenShockApplication.cs @@ -11,13 +11,21 @@ public static WebApplicationBuilder CreateDefaultBuilder(string[] args var builder = WebApplication.CreateSlimBuilder(args); builder.Configuration.Sources.Clear(); - builder.Configuration - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false) - .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) - .AddEnvironmentVariables() - .AddUserSecrets(true) - .AddCommandLine(args); + if (Environment.GetEnvironmentVariable("ASPNETCORE_UNDER_INTEGRATION_TEST") == "1") + { + builder.Configuration + .AddEnvironmentVariables(); + } + else + { + builder.Configuration + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false) + .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .AddUserSecrets(true) + .AddCommandLine(args); + } var isDevelopment = builder.Environment.IsDevelopment(); builder.Host.UseDefaultServiceProvider((_, options) => diff --git a/Common/Utils/ConnectionDetailsFetcher.cs b/Common/Utils/ConnectionDetailsFetcher.cs index 78480d36..e1738675 100644 --- a/Common/Utils/ConnectionDetailsFetcher.cs +++ b/Common/Utils/ConnectionDetailsFetcher.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Primitives; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Net; namespace OpenShock.Common.Utils; @@ -8,7 +7,7 @@ public static class ConnectionDetailsFetcher { public static IPAddress GetRemoteIP(this HttpContext context) { - return context.Connection?.RemoteIpAddress ?? throw new NullReferenceException("Unable to get any IP address, underlying transport type is not TCP???"); // This should never happen, as the underlying transport type will always be TCP + return context.Connection?.RemoteIpAddress ?? IPAddress.Loopback; // IPAddress is null under integration testing } public static string GetUserAgent(this HttpContext context) diff --git a/OpenShockBackend.sln b/OpenShockBackend.sln index a7b6f305..3f2eb7da 100644 --- a/OpenShockBackend.sln +++ b/OpenShockBackend.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MigrationHelper", "Migratio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{7AED9D47-F0B0-4644-A154-EE386A81C85D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.IntegrationTests", "API.IntegrationTests\API.IntegrationTests.csproj", "{9A9C5529-54A2-490C-A537-242D339054E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,8 +47,15 @@ Global {7AED9D47-F0B0-4644-A154-EE386A81C85D}.Debug|Any CPU.Build.0 = Debug|Any CPU {7AED9D47-F0B0-4644-A154-EE386A81C85D}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AED9D47-F0B0-4644-A154-EE386A81C85D}.Release|Any CPU.Build.0 = Release|Any CPU + {9A9C5529-54A2-490C-A537-242D339054E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A9C5529-54A2-490C-A537-242D339054E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A9C5529-54A2-490C-A537-242D339054E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A9C5529-54A2-490C-A537-242D339054E8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AD88368D-9B5B-4FB5-B4AA-7C8946471648} + EndGlobalSection EndGlobal