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:
parent
a80c133e2a
commit
ccb06b260c
24 changed files with 353 additions and 107 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,58 @@
|
|||
using IdentityShroud.Core.Services;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.VisualStudio.TestPlatform.TestHost;
|
||||
using Npgsql;
|
||||
using Testcontainers.PostgreSql;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Fixtures;
|
||||
|
||||
public class ApplicationFactory : WebApplicationFactory<Program>
|
||||
public class ApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
|
||||
{
|
||||
public IRealmService RealmService { get; } = Substitute.For<IRealmService>();
|
||||
private readonly PostgreSqlContainer _postgresqlServer;
|
||||
|
||||
// public IRealmService RealmService { get; } = Substitute.For<IRealmService>();
|
||||
|
||||
public ApplicationFactory()
|
||||
{
|
||||
_postgresqlServer = new PostgreSqlBuilder("postgres:18.1")
|
||||
.WithName($"is-applicationFactory-{Guid.NewGuid():N}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
builder.ConfigureAppConfiguration((context, configBuilder) =>
|
||||
{
|
||||
services.AddScoped<IRealmService>(c => RealmService);
|
||||
configBuilder.AddInMemoryCollection(
|
||||
new Dictionary<string, string?>
|
||||
{
|
||||
["Db:ConnectionString"] = _postgresqlServer.GetConnectionString(),
|
||||
["Encryption:Master"] = "GVd07qW0frRX9quPX/X62L88BeRR7+IzgRJHtG7ZzHw=",
|
||||
});
|
||||
});
|
||||
|
||||
// builder.ConfigureServices(services =>
|
||||
// {
|
||||
// services.AddScoped<IRealmService>(c => RealmService);
|
||||
// });
|
||||
|
||||
builder.UseEnvironment("Development");
|
||||
}
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
await _postgresqlServer.StartAsync();
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await _postgresqlServer.StopAsync();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
}
|
||||
41
IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs
Normal file
41
IdentityShroud.Api.Tests/Mappers/KeyMapperTests.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Api.Mappers;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Messages;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.TestUtils.Substitutes;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace IdentityShroud.Api.Tests.Mappers;
|
||||
|
||||
public class KeyMapperTests
|
||||
{
|
||||
private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough();
|
||||
|
||||
[Fact]
|
||||
public void Test()
|
||||
{
|
||||
// Setup
|
||||
using RSA rsa = RSA.Create(2048);
|
||||
|
||||
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||
|
||||
Key key = new()
|
||||
{
|
||||
Id = new("60bb79cf-4bac-4521-87f2-ac87cc15541f"),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Priority = 10,
|
||||
};
|
||||
key.SetPrivateKey(_encryptionService, rsa.ExportPkcs8PrivateKey());
|
||||
|
||||
// Act
|
||||
KeyMapper mapper = new(_encryptionService);
|
||||
JsonWebKey jwk = mapper.KeyToJsonWebKey(key);
|
||||
|
||||
Assert.Equal("RSA", jwk.KeyType);
|
||||
Assert.Equal(key.Id.ToString(), jwk.KeyId);
|
||||
Assert.Equal("sig", jwk.Use);
|
||||
Assert.Equal(parameters.Exponent, WebEncoders.Base64UrlDecode(jwk.Exponent));
|
||||
Assert.Equal(parameters.Modulus, WebEncoders.Base64UrlDecode(jwk.Modulus));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue