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