IdentityShroud/IdentityShroud.Api.Tests/Apis/RealmApisTests.cs

181 lines
6.6 KiB
C#
Raw Normal View History

using System.Buffers.Text;
2026-02-08 18:00:24 +01:00
using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using IdentityShroud.Core.EFCore;
using IdentityShroud.Core.Model;
2026-02-08 18:00:24 +01:00
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.TestUtils.Asserts;
2026-02-08 18:00:24 +01:00
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
2026-02-08 18:00:24 +01:00
namespace IdentityShroud.Api.Tests.Apis;
public class RealmApisTests : IClassFixture<ApplicationFactory>
2026-02-08 18:00:24 +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)
{
var client = _factory.CreateClient();
2026-02-08 18:00:24 +01:00
Guid? inputId = id is null ? (Guid?)null : new Guid(id);
// 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-08 18:00:24 +01:00
TestContext.Current.CancellationToken);
#if DEBUG
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);
// 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);
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);
// await factory.RealmService.DidNotReceive().Create(
// Arg.Any<RealmCreateRequest>(),
// Arg.Any<CancellationToken>());
2026-02-08 18:00:24 +01:00
}
}
[Fact]
public async Task GetOpenIdConfiguration_Success()
{
// setup
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 response = await client.GetAsync("auth/realms/foo/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
// verify
#if DEBUG
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
#endif
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
Assert.NotNull(result);
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");
}
[Theory]
[InlineData("")]
[InlineData("bar")]
public async Task GetOpenIdConfiguration_NotFound(string slug)
{
// act
var client = _factory.CreateClient();
var response = await client.GetAsync($"/realms/{slug}/.well-known/openid-configuration",
TestContext.Current.CancellationToken);
// verify
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetJwks()
{
var client = _factory.CreateClient();
var createResponse = await client.PostAsync("/api/v1/realms", JsonContent.Create(new
{
Slug = "foo",
Name = "Test'",
}),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
// act
var response = await client.GetAsync("/auth/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);
string? kid = JsonObjectAssert.NavigateToPath(payload, "keys[0].kid")?.AsValue().ToString();
Assert.NotNull(kid);
Assert.True(kid.Length >= 16);
//if (JsonObjectAssert.NavigateToPath(payload, "keys[0].kty")?.AsValue().ToString() == "RSA")
JsonObjectAssert.Equal("RSA", payload, "keys[0].kty");
string? n = payload["keys"]?[0]?["n"]?.AsValue().ToString();
string? e = payload["keys"]?[0]?["e"]?.AsValue().ToString();
AssertRsaParams(n, e);
}
private async Task ScopedContextAsync(
Func<Db, Task> action
)
{
using var scope = _factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
await action(db);
}
private static void AssertRsaParams(string? n, string? e)
{
Assert.NotNull(n);
Assert.NotNull(e);
var rsa = RSA.Create();
rsa.ImportParameters(new RSAParameters
{
Modulus = Base64Url.DecodeFromChars(n),
Exponent = Base64Url.DecodeFromChars(e)
});
// If n and e are complete nonsense, this will throw
var encrypted = rsa.Encrypt(new byte[] { 1, 2, 3 }, RSAEncryptionPadding.OaepSHA256);
Assert.NotNull(encrypted);
Assert.NotEmpty(encrypted);
}
2026-02-08 18:00:24 +01:00
}