From ad030563eecbd1d30a0225b1144ae136e5563b08 Mon Sep 17 00:00:00 2001 From: Ole Kristian Losvik Date: Wed, 25 Feb 2026 18:34:18 +0100 Subject: [PATCH] Auth: Prepare test infrastructure for multiple auth flows Implement test login endpoint and update configuration for client credentials --- src/Ignis.Auth/AuthConstants.cs | 9 ++++ .../Ignis.Api.Tests/AuthConfigurationTests.cs | 6 +++ .../AuthorizationControllerTests.cs | 20 ++++++--- tests/Ignis.Api.Tests/IgnisApiFactory.cs | 8 ++++ tests/Ignis.Api.Tests/IntegrationFixture.cs | 23 +++++++--- .../Ignis.Api.Tests/TestLoginStartupFilter.cs | 43 +++++++++++++++++++ 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/Ignis.Auth/AuthConstants.cs create mode 100644 tests/Ignis.Api.Tests/TestLoginStartupFilter.cs diff --git a/src/Ignis.Auth/AuthConstants.cs b/src/Ignis.Auth/AuthConstants.cs new file mode 100644 index 0000000..da943f9 --- /dev/null +++ b/src/Ignis.Auth/AuthConstants.cs @@ -0,0 +1,9 @@ +namespace Ignis.Auth; + +public static class AuthConstants +{ + /// + /// Authentication scheme used for the user session cookie during the authorization code flow. + /// + public const string SessionScheme = "IgnisAuth.Session"; +} diff --git a/tests/Ignis.Api.Tests/AuthConfigurationTests.cs b/tests/Ignis.Api.Tests/AuthConfigurationTests.cs index 37f476e..c36dbf9 100644 --- a/tests/Ignis.Api.Tests/AuthConfigurationTests.cs +++ b/tests/Ignis.Api.Tests/AuthConfigurationTests.cs @@ -123,6 +123,7 @@ public async Task TokenEndpoint_Available_WhenAuthEnabled() ["AuthSettings__Clients__0__ClientId"] = "config-client", ["AuthSettings__Clients__0__ClientSecret"] = "config-secret", ["AuthSettings__Clients__0__DisplayName"] = "Config Client", + ["AuthSettings__Clients__0__AllowedGrantTypes__0"] = "client_credentials", ["StoreSettings__ConnectionString"] = _connectionString, }; SetEnvVars(envVars); @@ -139,6 +140,7 @@ public async Task TokenEndpoint_Available_WhenAuthEnabled() ["AuthSettings:Clients:0:ClientId"] = "config-client", ["AuthSettings:Clients:0:ClientSecret"] = "config-secret", ["AuthSettings:Clients:0:DisplayName"] = "Config Client", + ["AuthSettings:Clients:0:AllowedGrantTypes:0"] = "client_credentials", }); using var client = factory.CreateClient(); @@ -174,6 +176,7 @@ public async Task TokenEndpoint_Works_WithCertificatesInProduction() ["AuthSettings__Clients__0__ClientId"] = "cert-client", ["AuthSettings__Clients__0__ClientSecret"] = "cert-secret", ["AuthSettings__Clients__0__DisplayName"] = "Cert Client", + ["AuthSettings__Clients__0__AllowedGrantTypes__0"] = "client_credentials", ["AuthSettings__Certificates__SigningCertificatePath"] = signingCertPath, ["AuthSettings__Certificates__SigningCertificatePassword"] = signingCertPassword, ["AuthSettings__Certificates__EncryptionCertificatePath"] = encryptionCertPath, @@ -194,6 +197,7 @@ public async Task TokenEndpoint_Works_WithCertificatesInProduction() ["AuthSettings:Clients:0:ClientId"] = "cert-client", ["AuthSettings:Clients:0:ClientSecret"] = "cert-secret", ["AuthSettings:Clients:0:DisplayName"] = "Cert Client", + ["AuthSettings:Clients:0:AllowedGrantTypes:0"] = "client_credentials", ["AuthSettings:Certificates:SigningCertificatePath"] = signingCertPath, ["AuthSettings:Certificates:SigningCertificatePassword"] = signingCertPassword, ["AuthSettings:Certificates:EncryptionCertificatePath"] = encryptionCertPath, @@ -235,6 +239,7 @@ public void Startup_Fails_WhenCertificatesMissing_InProduction() ["AuthSettings__ConnectionString"] = _connectionString, ["AuthSettings__Clients__0__ClientId"] = "cert-client", ["AuthSettings__Clients__0__ClientSecret"] = "cert-secret", + ["AuthSettings__Clients__0__AllowedGrantTypes__0"] = "client_credentials", ["StoreSettings__ConnectionString"] = _connectionString, }; SetEnvVars(envVars); @@ -252,6 +257,7 @@ public void Startup_Fails_WhenCertificatesMissing_InProduction() ["AuthSettings:ConnectionString"] = _connectionString, ["AuthSettings:Clients:0:ClientId"] = "cert-client", ["AuthSettings:Clients:0:ClientSecret"] = "cert-secret", + ["AuthSettings:Clients:0:AllowedGrantTypes:0"] = "client_credentials", }, environment: "Production"); factory.CreateClient(); }; diff --git a/tests/Ignis.Api.Tests/AuthorizationControllerTests.cs b/tests/Ignis.Api.Tests/AuthorizationControllerTests.cs index 3de3f8c..d112332 100644 --- a/tests/Ignis.Api.Tests/AuthorizationControllerTests.cs +++ b/tests/Ignis.Api.Tests/AuthorizationControllerTests.cs @@ -8,11 +8,11 @@ namespace Ignis.Api.Tests; [Collection("IntegrationTests")] public class AuthorizationControllerTests : IClassFixture { - private readonly HttpClient _client; + private readonly IntegrationFixture _fixture; public AuthorizationControllerTests(IntegrationFixture fixture) { - _client = fixture.Factory.CreateClient(); + _fixture = fixture; } private static CancellationToken CT => TestContext.Current.CancellationToken; @@ -20,7 +20,9 @@ public AuthorizationControllerTests(IntegrationFixture fixture) [Fact] public async Task Token_WithValidClientCredentials_ReturnsAccessToken() { - var response = await _client.PostAsync("/connect/token", + using var client = _fixture.Factory.CreateClient(); + + var response = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "client_credentials", @@ -38,7 +40,9 @@ public async Task Token_WithValidClientCredentials_ReturnsAccessToken() [Fact] public async Task Token_WithInvalidClient_ReturnsUnauthorized() { - var response = await _client.PostAsync("/connect/token", + using var client = _fixture.Factory.CreateClient(); + + var response = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "client_credentials", @@ -52,7 +56,9 @@ public async Task Token_WithInvalidClient_ReturnsUnauthorized() [Fact] public async Task Token_WithWrongSecret_ReturnsUnauthorized() { - var response = await _client.PostAsync("/connect/token", + using var client = _fixture.Factory.CreateClient(); + + var response = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "client_credentials", @@ -66,7 +72,9 @@ public async Task Token_WithWrongSecret_ReturnsUnauthorized() [Fact] public async Task Token_WithUnsupportedGrantType_ReturnsBadRequest() { - var response = await _client.PostAsync("/connect/token", + using var client = _fixture.Factory.CreateClient(); + + var response = await client.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "password", diff --git a/tests/Ignis.Api.Tests/IgnisApiFactory.cs b/tests/Ignis.Api.Tests/IgnisApiFactory.cs index 4149bd7..5eadc9e 100644 --- a/tests/Ignis.Api.Tests/IgnisApiFactory.cs +++ b/tests/Ignis.Api.Tests/IgnisApiFactory.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; namespace Ignis.Api.Tests; @@ -28,9 +29,16 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) ["AuthSettings:Clients:0:ClientId"] = "test-client", ["AuthSettings:Clients:0:ClientSecret"] = "test-secret", ["AuthSettings:Clients:0:DisplayName"] = "Test Client", + ["AuthSettings:Clients:0:AllowedGrantTypes:0"] = "client_credentials", + ["AuthSettings:Clients:0:RedirectUris:0"] = "http://localhost/callback", }); }); + builder.ConfigureServices(services => + { + services.AddSingleton(); + }); + builder.UseEnvironment("Development"); } } diff --git a/tests/Ignis.Api.Tests/IntegrationFixture.cs b/tests/Ignis.Api.Tests/IntegrationFixture.cs index 6d640d6..0367bb6 100644 --- a/tests/Ignis.Api.Tests/IntegrationFixture.cs +++ b/tests/Ignis.Api.Tests/IntegrationFixture.cs @@ -30,6 +30,19 @@ private static string BuildConnectionString(string raw) return mongoUrl.ToString(); } + private static readonly string[] EnvVarKeys = + [ + "StoreSettings__ConnectionString", + "AuthSettings__Enabled", + "AuthSettings__ConnectionString", + "AuthSettings__Clients__0__ClientId", + "AuthSettings__Clients__0__ClientSecret", + "AuthSettings__Clients__0__DisplayName", + "AuthSettings__Clients__0__AllowedGrantTypes__0", + "AuthSettings__Clients__0__AllowedGrantTypes__1", + "AuthSettings__Clients__0__RedirectUris__0", + ]; + public async ValueTask InitializeAsync() { await _mongo.StartAsync(); @@ -41,18 +54,16 @@ public async ValueTask InitializeAsync() Environment.SetEnvironmentVariable("AuthSettings__Clients__0__ClientId", "test-client"); Environment.SetEnvironmentVariable("AuthSettings__Clients__0__ClientSecret", "test-secret"); Environment.SetEnvironmentVariable("AuthSettings__Clients__0__DisplayName", "Test Client"); + Environment.SetEnvironmentVariable("AuthSettings__Clients__0__AllowedGrantTypes__0", "client_credentials"); + Environment.SetEnvironmentVariable("AuthSettings__Clients__0__RedirectUris__0", "http://localhost/callback"); Factory = new IgnisApiFactory(connectionString); } public async ValueTask DisposeAsync() { - Environment.SetEnvironmentVariable("StoreSettings__ConnectionString", null); - Environment.SetEnvironmentVariable("AuthSettings__Enabled", null); - Environment.SetEnvironmentVariable("AuthSettings__ConnectionString", null); - Environment.SetEnvironmentVariable("AuthSettings__Clients__0__ClientId", null); - Environment.SetEnvironmentVariable("AuthSettings__Clients__0__ClientSecret", null); - Environment.SetEnvironmentVariable("AuthSettings__Clients__0__DisplayName", null); + foreach (var key in EnvVarKeys) + Environment.SetEnvironmentVariable(key, null); Factory.Dispose(); await _mongo.DisposeAsync(); } diff --git a/tests/Ignis.Api.Tests/TestLoginStartupFilter.cs b/tests/Ignis.Api.Tests/TestLoginStartupFilter.cs new file mode 100644 index 0000000..1f795ec --- /dev/null +++ b/tests/Ignis.Api.Tests/TestLoginStartupFilter.cs @@ -0,0 +1,43 @@ +using System.Security.Claims; + +using Ignis.Auth; + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; + +namespace Ignis.Api.Tests; + +/// +/// Adds a /test-login endpoint that signs in as a test user +/// using the cookie scheme. +/// +internal sealed class TestLoginStartupFilter : IStartupFilter +{ + public Action Configure(Action next) + { + return app => + { + app.Use(async (context, nextMiddleware) => + { + if (context.Request.Path != "/test-login") + { + await nextMiddleware(); + return; + } + + var claims = new List + { + new(ClaimTypes.NameIdentifier, "test-user-id"), + new(ClaimTypes.Name, "Test User"), + }; + var identity = new ClaimsIdentity(claims, AuthConstants.SessionScheme); + await context.SignInAsync( + AuthConstants.SessionScheme, new ClaimsPrincipal(identity)); + context.Response.StatusCode = StatusCodes.Status200OK; + }); + next(app); + }; + } +}