2026-02-08 18:00:24 +01:00
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http.Json;
|
2026-02-15 19:06:09 +01:00
|
|
|
using System.Security.Cryptography;
|
2026-02-14 14:50:06 +01:00
|
|
|
using System.Text.Json.Nodes;
|
2026-02-15 19:06:09 +01:00
|
|
|
using IdentityShroud.Core;
|
|
|
|
|
using IdentityShroud.Core.Contracts;
|
2026-02-14 14:50:06 +01:00
|
|
|
using IdentityShroud.Core.Model;
|
2026-02-08 18:00:24 +01:00
|
|
|
using IdentityShroud.Core.Tests.Fixtures;
|
2026-02-14 14:50:06 +01:00
|
|
|
using IdentityShroud.TestUtils.Asserts;
|
2026-02-08 18:00:24 +01:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2026-02-15 19:06:09 +01:00
|
|
|
using Microsoft.AspNetCore.WebUtilities;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-02-08 18:00:24 +01:00
|
|
|
|
|
|
|
|
namespace IdentityShroud.Api.Tests.Apis;
|
|
|
|
|
|
2026-02-15 19:06:09 +01:00
|
|
|
public class RealmApisTests : IClassFixture<ApplicationFactory>
|
2026-02-08 18:00:24 +01:00
|
|
|
{
|
2026-02-15 19:06:09 +01:00
|
|
|
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;");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 18:00:24 +01:00
|
|
|
[Theory]
|
|
|
|
|
[InlineData(null, null, null, false, "Name")]
|
|
|
|
|
[InlineData(null, null, "Foo", true, "")]
|
|
|
|
|
[InlineData(null, null, "", false, "Name")]
|
|
|
|
|
[InlineData(null, "foo", "Foo", true, "")]
|
|
|
|
|
[InlineData(null, "f/oo", "Foo", false, "Slug")]
|
|
|
|
|
[InlineData(null, "", "Foo", false, "Slug")]
|
|
|
|
|
[InlineData("0814934a-efe2-4784-ba84-a184c0b9de9e", "foo", "Foo", true, "")]
|
|
|
|
|
[InlineData("00000000-0000-0000-0000-000000000000", "foo", "Foo", false, "Id")]
|
|
|
|
|
public async Task Create(string? id, string? slug, string? name, bool succeeds, string fieldName)
|
|
|
|
|
{
|
2026-02-15 19:06:09 +01:00
|
|
|
var client = _factory.CreateClient();
|
2026-02-08 18:00:24 +01:00
|
|
|
|
|
|
|
|
Guid? inputId = id is null ? (Guid?)null : new Guid(id);
|
2026-02-21 20:15:46 +01:00
|
|
|
|
|
|
|
|
// act
|
|
|
|
|
var response = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
|
2026-02-08 18:00:24 +01:00
|
|
|
{
|
|
|
|
|
Id = inputId,
|
|
|
|
|
Slug = slug,
|
|
|
|
|
Name = name,
|
2026-02-14 14:50:06 +01:00
|
|
|
}),
|
2026-02-08 18:00:24 +01:00
|
|
|
TestContext.Current.CancellationToken);
|
|
|
|
|
#if DEBUG
|
2026-02-14 14:50:06 +01:00
|
|
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
|
|
|
#endif
|
|
|
|
|
|
2026-02-08 18:00:24 +01:00
|
|
|
if (succeeds)
|
|
|
|
|
{
|
|
|
|
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
2026-02-15 19:06:09 +01:00
|
|
|
// await factory.RealmService.Received(1).Create(
|
|
|
|
|
// Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
|
|
|
|
|
// Arg.Any<CancellationToken>());
|
2026-02-08 18:00:24 +01:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
2026-02-14 14:50:06 +01:00
|
|
|
var problemDetails =
|
|
|
|
|
await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(
|
|
|
|
|
TestContext.Current.CancellationToken);
|
2026-02-08 18:00:24 +01:00
|
|
|
|
|
|
|
|
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
|
2026-02-15 19:06:09 +01:00
|
|
|
// await factory.RealmService.DidNotReceive().Create(
|
|
|
|
|
// Arg.Any<RealmCreateRequest>(),
|
|
|
|
|
// Arg.Any<CancellationToken>());
|
2026-02-08 18:00:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 14:50:06 +01:00
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetOpenIdConfiguration_Success()
|
|
|
|
|
{
|
|
|
|
|
// setup
|
2026-02-15 19:06:09 +01:00
|
|
|
await ScopedContextAsync(async db =>
|
|
|
|
|
{
|
|
|
|
|
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo" });
|
|
|
|
|
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-14 14:50:06 +01:00
|
|
|
// act
|
2026-02-15 19:06:09 +01:00
|
|
|
var client = _factory.CreateClient();
|
2026-02-21 20:15:46 +01:00
|
|
|
var response = await client.GetAsync("auth/realms/foo/.well-known/openid-configuration",
|
2026-02-14 14:50:06 +01:00
|
|
|
TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
// verify
|
2026-02-21 20:15:46 +01:00
|
|
|
#if DEBUG
|
|
|
|
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
|
|
|
#endif
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
|
|
|
|
2026-02-14 14:50:06 +01:00
|
|
|
var result = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
|
|
|
|
Assert.NotNull(result);
|
2026-02-21 20:15:46 +01:00
|
|
|
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/auth", result, "authorization_endpoint");
|
|
|
|
|
JsonObjectAssert.Equal("http://localhost/auth/realms/foo", result, "issuer");
|
|
|
|
|
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/token", result, "token_endpoint");
|
|
|
|
|
JsonObjectAssert.Equal("http://localhost/auth/realms/foo/openid-connect/jwks", result, "jwks_uri");
|
2026-02-14 14:50:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("")]
|
|
|
|
|
[InlineData("bar")]
|
|
|
|
|
public async Task GetOpenIdConfiguration_NotFound(string slug)
|
|
|
|
|
{
|
|
|
|
|
// act
|
2026-02-15 19:06:09 +01:00
|
|
|
var client = _factory.CreateClient();
|
2026-02-24 06:32:58 +01:00
|
|
|
var response = await client.GetAsync($"/realms/{slug}/.well-known/openid-configuration",
|
2026-02-14 14:50:06 +01:00
|
|
|
TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
// verify
|
|
|
|
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
|
|
|
|
}
|
2026-02-15 19:06:09 +01:00
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task GetJwks()
|
|
|
|
|
{
|
|
|
|
|
// setup
|
2026-02-26 16:53:02 +01:00
|
|
|
IDekEncryptionService dekEncryptionService = _factory.Services.GetRequiredService<IDekEncryptionService>();
|
2026-02-15 19:06:09 +01:00
|
|
|
|
|
|
|
|
using var rsa = RSA.Create(2048);
|
|
|
|
|
RSAParameters parameters = rsa.ExportParameters(includePrivateParameters: false);
|
2026-02-20 17:35:38 +01:00
|
|
|
|
2026-02-24 06:32:58 +01:00
|
|
|
RealmKey realmKey = new()
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
KeyType = "RSA",
|
2026-02-26 16:53:02 +01:00
|
|
|
Key = dekEncryptionService.Encrypt(rsa.ExportPkcs8PrivateKey()),
|
2026-02-24 06:32:58 +01:00
|
|
|
CreatedAt = DateTime.UtcNow,
|
|
|
|
|
};
|
2026-02-15 19:06:09 +01:00
|
|
|
|
|
|
|
|
await ScopedContextAsync(async db =>
|
|
|
|
|
{
|
2026-02-20 17:35:38 +01:00
|
|
|
db.Realms.Add(new Realm() { Slug = "foo", Name = "Foo", Keys = [ realmKey ]});
|
2026-02-15 19:06:09 +01:00
|
|
|
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
|
|
|
|
});
|
2026-02-24 06:32:58 +01:00
|
|
|
|
2026-02-15 19:06:09 +01:00
|
|
|
// act
|
|
|
|
|
var client = _factory.CreateClient();
|
2026-02-21 20:15:46 +01:00
|
|
|
var response = await client.GetAsync("/auth/realms/foo/openid-connect/jwks",
|
2026-02-15 19:06:09 +01:00
|
|
|
TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
|
|
|
JsonObject? payload = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
|
|
|
|
|
|
|
|
|
Assert.NotNull(payload);
|
2026-02-20 17:35:38 +01:00
|
|
|
JsonObjectAssert.Equal(realmKey.Id.ToString(), payload, "keys[0].kid");
|
2026-02-15 19:06:09 +01:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-08 18:00:24 +01:00
|
|
|
}
|