Implement jwks endpoint and add test for it.

This also let to some improvements/cleanups of the other tests and fixtures.
This commit is contained in:
eelke 2026-02-15 19:06:09 +01:00
parent a80c133e2a
commit ccb06b260c
24 changed files with 353 additions and 107 deletions

View file

@ -1,19 +1,35 @@
using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using FluentResults;
using IdentityShroud.Core.Messages.Realm;
using IdentityShroud.Core;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Asserts;
using Microsoft.AspNetCore.Mvc;
using NSubstitute.ClearExtensions;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace IdentityShroud.Api.Tests.Apis;
public class RealmApisTests(ApplicationFactory factory) : IClassFixture<ApplicationFactory>
public class RealmApisTests : IClassFixture<ApplicationFactory>
{
private readonly ApplicationFactory _factory;
public RealmApisTests(ApplicationFactory factory)
{
_factory = factory;
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
if (!db.Database.EnsureCreated())
{
db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
}
}
[Theory]
[InlineData(null, null, null, false, "Name")]
[InlineData(null, null, "Foo", true, "")]
@ -25,11 +41,7 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
[InlineData("00000000-0000-0000-0000-000000000000", "foo", "Foo", false, "Id")]
public async Task Create(string? id, string? slug, string? name, bool succeeds, string fieldName)
{
var client = factory.CreateClient();
factory.RealmService.ClearSubstitute();
factory.RealmService.Create(Arg.Any<RealmCreateRequest>(), Arg.Any<CancellationToken>())
.Returns(Result.Ok(new RealmCreateResponse(Guid.NewGuid(), "foo", "Foo")));
var client = _factory.CreateClient();
Guid? inputId = id is null ? (Guid?)null : new Guid(id);
var response = await client.PostAsync("/realms", JsonContent.Create(new
@ -46,9 +58,9 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
if (succeeds)
{
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
await factory.RealmService.Received(1).Create(
Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
Arg.Any<CancellationToken>());
// await factory.RealmService.Received(1).Create(
// Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
// Arg.Any<CancellationToken>());
}
else
{
@ -58,9 +70,9 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
TestContext.Current.CancellationToken);
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
await factory.RealmService.DidNotReceive().Create(
Arg.Any<RealmCreateRequest>(),
Arg.Any<CancellationToken>());
// await factory.RealmService.DidNotReceive().Create(
// Arg.Any<RealmCreateRequest>(),
// Arg.Any<CancellationToken>());
}
}
@ -68,11 +80,14 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
public async Task GetOpenIdConfiguration_Success()
{
// setup
factory.RealmService.FindBySlug(Arg.Is<string>("foo"), Arg.Any<CancellationToken>())
.Returns(new Realm());
await ScopedContextAsync(async db =>
{
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo" });
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
});
// act
var client = factory.CreateClient();
var client = _factory.CreateClient();
var response = await client.GetAsync("/realms/foo/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
@ -91,11 +106,56 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
public async Task GetOpenIdConfiguration_NotFound(string slug)
{
// act
var client = factory.CreateClient();
var client = _factory.CreateClient();
var response = await client.GetAsync("/realms/bar/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
// verify
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetJwks()
{
// setup
IEncryptionService encryptionService = _factory.Services.GetRequiredService<IEncryptionService>();
using var rsa = RSA.Create(2048);
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
Key key = new()
{
Id = Guid.NewGuid(),
CreatedAt = DateTime.UtcNow,
};
key.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
await ScopedContextAsync(async db =>
{
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ key ]});
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
});
// act
var client = _factory.CreateClient();
var response = await client.GetAsync("/realms/foo/openid-connect/jwks",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
JsonObject? payload = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
Assert.NotNull(payload);
JsonObjectAssert.Equal(key.Id.ToString(), payload, "keys[0].kid");
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Modulus!), payload, "keys[0].n");
JsonObjectAssert.Equal(WebEncoders.Base64UrlEncode(parameters.Exponent!), payload, "keys[0].e");
}
private async Task ScopedContextAsync(
Func<Db, Task> action
)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
await action(db);
}
}