Happy flow for creating realms works

But needs more validating...
This commit is contained in:
eelke 2026-02-08 11:57:57 +01:00
parent f99c97f392
commit 92b34bd0b5
25 changed files with 437 additions and 12 deletions

View file

@ -0,0 +1,65 @@
using FluentResults;
namespace IdentityShroud.Core.Tests;
public static class ResultAssert
{
public static void Success(Result result)
{
if (!result.IsSuccess)
{
var errors = string.Join("\n", result.Errors.Select(e => "\t" + e.Message));
Assert.True(result.IsSuccess, $"ResultAssert.Success: failed, got errors:\n{errors}");
}
}
public static T Success<T>(Result<T> result)
{
Success(result.ToResult());
return result.Value;
}
public static void Failed(Result result, Predicate<IError>? filter = null)
{
if (!result.IsFailed)
{
Assert.Fail("ResultAssert.Failed: failed, unexpected success result");
}
if (filter is not null)
Assert.Contains(result.Errors, filter);
}
public static void Failed<T>(Result<T> result, Predicate<IError>? filter = null)
{
Failed(result.ToResult(), filter);
}
public static void FailedWith<TError>(Result result) where TError : IError
{
if (!result.IsFailed)
{
Assert.Fail("ResultAssert.Failed: failed, unexpected success result");
}
if (!result.Errors.Any(e => e is TError))
{
string typeName = typeof(TError).Name;
Assert.Fail($"ResultAssert.Failed: failed, no error of the type {typeName} found");
}
}
public static void FailedWith<T, TError>(Result<T> result) where TError : IError
{
if (!result.IsFailed)
{
Assert.Fail("ResultAssert.Failed: failed, unexpected success result");
}
if (!result.Errors.Any(e => e is TError))
{
string typeName = typeof(TError).Name;
Assert.Fail($"ResultAssert.Failed: failed, no error of the type {typeName} found");
}
}
}

View file

@ -0,0 +1,70 @@
using DotNet.Testcontainers.Containers;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Npgsql;
using Testcontainers.PostgreSql;
namespace IdentityShroud.Core.Tests.Fixtures;
public class DbFixture : IAsyncLifetime
{
private readonly IContainer _postgresqlServer;
private string ConnectionString =>
$"Host={_postgresqlServer.Hostname};" +
$"Port={DbPort};" +
$"Username={Username};Password={Password}";
private string Username => "postgres";
private string Password => "password";
private string DbHostname => _postgresqlServer.Hostname;
private int DbPort => _postgresqlServer.GetMappedPublicPort(PostgreSqlBuilder.PostgreSqlPort);
public Db CreateDbContext(string dbName)
{
var db = new Db(Options.Create<DbConfiguration>(new()
{
ConnectionString = ConnectionString + ";Database=" + dbName,
LogSensitiveData = false,
}), new NullLoggerFactory());
return db;
}
public DbFixture()
{
_postgresqlServer = new PostgreSqlBuilder("postgres:18.1")
.WithName("KMS-Test-Infra-" + Guid.NewGuid().ToString("D"))
.WithPassword(Password)
.Build();
}
public async ValueTask DisposeAsync()
{
await _postgresqlServer.StopAsync();
}
public async ValueTask InitializeAsync()
{
await _postgresqlServer.StartAsync();
}
public NpgsqlConnection GetConnection(string dbname)
{
string connString = ConnectionString
+ $";Database={dbname}";
var connection = new NpgsqlConnection(connString);
connection.Open();
return connection;
}
}
/*
[CollectionDefinition("PostgresqlFixtureCollection", DisableParallelization = false)]
public class PostgresqlFactoryCollection : ICollectionFixture<PostgresqlFixture>
{
// This class has no code, and is never created. Its purpose is simply
// to be the place to apply [CollectionDefinition] and all the
// ICollectionFixture<> interfaces.
}
*/

View file

@ -12,12 +12,16 @@
<PackageReference Include="jose-jwt" Version="5.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="10.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
<PackageReference Include="xunit" Version="2.9.3"/>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Testcontainers" Version="4.10.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="4.10.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
<PackageReference Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
<Using Include="NSubstitute"/>
</ItemGroup>
<ItemGroup>

View file

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=asserts/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -0,0 +1,51 @@
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Model;
namespace IdentityShroud.Core.Tests.Model;
public class RealmTests
{
[Fact]
public void SetNewKey()
{
byte[] privateKey = [5, 6, 7, 8];
byte[] encryptedPrivateKey = [1, 2, 3, 4];
var encryptionService = Substitute.For<IEncryptionService>();
encryptionService
.Encrypt(Arg.Any<byte[]>())
.Returns(x => encryptedPrivateKey);
Realm realm = new();
realm.SetPrivateKey(encryptionService, privateKey);
// should be able to return original without calling decrypt
Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
encryptionService.Received(1).Encrypt(privateKey);
encryptionService.DidNotReceive().Decrypt(Arg.Any<byte[]>());
}
[Fact]
public void GetDecryptedKey()
{
byte[] privateKey = [5, 6, 7, 8];
byte[] encryptedPrivateKey = [1, 2, 3, 4];
var encryptionService = Substitute.For<IEncryptionService>();
encryptionService
.Decrypt(encryptedPrivateKey)
.Returns(x => privateKey);
Realm realm = new();
realm.PrivateKeyEncrypted = encryptedPrivateKey;
// should be able to return original without calling decrypt
Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
encryptionService.Received(1).Decrypt(encryptedPrivateKey);
}
}

View file

@ -0,0 +1,22 @@
using System.Security.Cryptography;
using IdentityShroud.Core.Services;
namespace IdentityShroud.Core.Tests.Services;
public class EncryptionServiceTests
{
[Fact]
public void RoundtripWorks()
{
// setup
string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
EncryptionService sut = new(key);
byte[] input = RandomNumberGenerator.GetBytes(16);
// act
var cipher = sut.Encrypt(input);
var result = sut.Decrypt(cipher);
Assert.Equal(input, result);
}
}

View file

@ -0,0 +1,52 @@
using FluentResults;
using IdentityShroud.Core.Services;
using IdentityShroud.Core.Tests.Fixtures;
using IdentityShroud.Core.Tests.Substitutes;
using Microsoft.EntityFrameworkCore;
namespace IdentityShroud.Core.Tests.Services;
public class RealmServiceTests : IClassFixture<DbFixture>
{
private readonly Db _db;
public RealmServiceTests(DbFixture dbFixture)
{
_db = dbFixture.CreateDbContext("realmservice");
if (!_db.Database.EnsureCreated())
TruncateTables();
}
private void TruncateTables()
{
_db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
}
[Theory]
[InlineData(null)]
[InlineData("a7c2a39c-3ed9-4790-826e-43bb2e5e480c")]
public async Task Create(string? idString)
{
Guid? realmId = null;
if (idString is not null)
realmId = new(idString);
var encryptionService = EncryptionServiceSubstitute.CreatePassthrough();
RealmService sut = new(_db, encryptionService);
var response = await sut.Create(
new(realmId, "slug", "New realm"),
TestContext.Current.CancellationToken);
RealmCreateResponse val = ResultAssert.Success(response);
if (realmId.HasValue)
Assert.Equal(realmId, val.Realm.Id);
else
Assert.NotEqual(Guid.Empty, val.Realm.Id);
Assert.Equal("slug", val.Realm.Slug);
Assert.Equal("New realm", val.Realm.Name);
Assert.NotEmpty(val.Realm.PrivateKeyEncrypted);
}
}

View file

@ -0,0 +1,18 @@
using IdentityShroud.Core.Contracts;
namespace IdentityShroud.Core.Tests.Substitutes;
public static class EncryptionServiceSubstitute
{
public static IEncryptionService CreatePassthrough()
{
var encryptionService = Substitute.For<IEncryptionService>();
encryptionService
.Encrypt(Arg.Any<byte[]>())
.Returns(x => x.ArgAt<byte[]>(0));
encryptionService
.Decrypt(Arg.Any<byte[]>())
.Returns(x => x.ArgAt<byte[]>(0));
return encryptionService;
}
}