Add tests and fixes to .well-known/openid-configuration and create realm
This commit is contained in:
parent
e07d6e3ea5
commit
d440979451
17 changed files with 642 additions and 45 deletions
|
|
@ -1,9 +1,12 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
using IdentityShroud.Core.Services;
|
using IdentityShroud.Core.Services;
|
||||||
using IdentityShroud.Core.Tests.Fixtures;
|
using IdentityShroud.Core.Tests.Fixtures;
|
||||||
|
using IdentityShroud.TestUtils.Asserts;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NSubstitute.ClearExtensions;
|
using NSubstitute.ClearExtensions;
|
||||||
|
|
||||||
|
|
@ -34,28 +37,65 @@ public class RealmApisTests(ApplicationFactory factory) : IClassFixture<Applicat
|
||||||
Id = inputId,
|
Id = inputId,
|
||||||
Slug = slug,
|
Slug = slug,
|
||||||
Name = name,
|
Name = name,
|
||||||
}),
|
}),
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (succeeds)
|
if (succeeds)
|
||||||
{
|
{
|
||||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||||
await factory.RealmService.Received(1).Create(
|
await factory.RealmService.Received(1).Create(
|
||||||
Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
|
Arg.Is<RealmCreateRequest>(r => r.Id == inputId && r.Slug == slug && r.Name == name),
|
||||||
Arg.Any<CancellationToken>());
|
Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
var problemDetails = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(TestContext.Current.CancellationToken);
|
var problemDetails =
|
||||||
|
await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
|
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
|
||||||
await factory.RealmService.DidNotReceive().Create(
|
await factory.RealmService.DidNotReceive().Create(
|
||||||
Arg.Any<RealmCreateRequest>(),
|
Arg.Any<RealmCreateRequest>(),
|
||||||
Arg.Any<CancellationToken>());
|
Arg.Any<CancellationToken>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetOpenIdConfiguration_Success()
|
||||||
|
{
|
||||||
|
// setup
|
||||||
|
factory.RealmService.FindBySlug(Arg.Is<string>("foo"), Arg.Any<CancellationToken>())
|
||||||
|
.Returns(new Realm());
|
||||||
|
|
||||||
|
// act
|
||||||
|
var client = factory.CreateClient();
|
||||||
|
var response = await client.GetAsync("/realms/foo/.well-known/openid-configuration",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// verify
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<JsonObject>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
JsonObjectAssert.Equal("http://localhost/realms/foo/openid-connect/auth", result, "authorization_endpoint");
|
||||||
|
JsonObjectAssert.Equal("http://localhost/realms/foo", result, "issuer");
|
||||||
|
JsonObjectAssert.Equal("http://localhost/realms/foo/openid-connect/token", result, "token_endpoint");
|
||||||
|
JsonObjectAssert.Equal("http://localhost/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/bar/.well-known/openid-configuration",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// verify
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\IdentityShroud.Api\IdentityShroud.Api.csproj" />
|
<ProjectReference Include="..\IdentityShroud.Api\IdentityShroud.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\IdentityShroud.TestUtils\IdentityShroud.TestUtils.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ public static class RealmApi
|
||||||
.WithName("Create Realm")
|
.WithName("Create Realm")
|
||||||
.Produces(StatusCodes.Status201Created);
|
.Produces(StatusCodes.Status201Created);
|
||||||
|
|
||||||
var realmSlugGroup = app.MapGroup("{slug}");
|
var realmSlugGroup = realmsGroup.MapGroup("{slug}");
|
||||||
realmSlugGroup.MapGet("", GetRealmInfo);
|
realmSlugGroup.MapGet("", GetRealmInfo);
|
||||||
realmSlugGroup.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
realmSlugGroup.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
||||||
|
|
||||||
|
|
@ -54,10 +54,18 @@ public static class RealmApi
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Results<JsonHttpResult<OpenIdConfiguration>, BadRequest>> GetOpenIdConfiguration(string slug, HttpContext context)
|
private static async Task<Results<JsonHttpResult<OpenIdConfiguration>, BadRequest, NotFound>> GetOpenIdConfiguration(
|
||||||
|
[FromServices]IRealmService realmService,
|
||||||
|
HttpContext context,
|
||||||
|
string slug)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(slug))
|
if (string.IsNullOrEmpty(slug))
|
||||||
return TypedResults.BadRequest();
|
return TypedResults.BadRequest();
|
||||||
|
|
||||||
|
var realm = await realmService.FindBySlug(slug);
|
||||||
|
if (realm is null)
|
||||||
|
return TypedResults.NotFound();
|
||||||
|
|
||||||
var s = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
|
var s = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}";
|
||||||
var searchString = $"realms/{slug}";
|
var searchString = $"realms/{slug}";
|
||||||
int index = s.IndexOf(searchString, StringComparison.OrdinalIgnoreCase);
|
int index = s.IndexOf(searchString, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\IdentityShroud.Core\IdentityShroud.Core.csproj" />
|
<ProjectReference Include="..\IdentityShroud.Core\IdentityShroud.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\IdentityShroud.TestUtils\IdentityShroud.TestUtils.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -3,7 +3,7 @@ using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Tests.Model;
|
namespace IdentityShroud.Core.Tests.Model;
|
||||||
|
|
||||||
public class RealmTests
|
public class KeyTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SetNewKey()
|
public void SetNewKey()
|
||||||
|
|
@ -16,12 +16,12 @@ public class RealmTests
|
||||||
.Encrypt(Arg.Any<byte[]>())
|
.Encrypt(Arg.Any<byte[]>())
|
||||||
.Returns(x => encryptedPrivateKey);
|
.Returns(x => encryptedPrivateKey);
|
||||||
|
|
||||||
Realm realm = new();
|
Key key = new();
|
||||||
realm.SetPrivateKey(encryptionService, privateKey);
|
key.SetPrivateKey(encryptionService, privateKey);
|
||||||
|
|
||||||
// should be able to return original without calling decrypt
|
// should be able to return original without calling decrypt
|
||||||
Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
|
Assert.Equal(privateKey, key.GetPrivateKey(encryptionService));
|
||||||
Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
|
Assert.Equal(encryptedPrivateKey, key.PrivateKeyEncrypted);
|
||||||
|
|
||||||
encryptionService.Received(1).Encrypt(privateKey);
|
encryptionService.Received(1).Encrypt(privateKey);
|
||||||
encryptionService.DidNotReceive().Decrypt(Arg.Any<byte[]>());
|
encryptionService.DidNotReceive().Decrypt(Arg.Any<byte[]>());
|
||||||
|
|
@ -38,12 +38,12 @@ public class RealmTests
|
||||||
.Decrypt(encryptedPrivateKey)
|
.Decrypt(encryptedPrivateKey)
|
||||||
.Returns(x => privateKey);
|
.Returns(x => privateKey);
|
||||||
|
|
||||||
Realm realm = new();
|
Key key = new();
|
||||||
realm.PrivateKeyEncrypted = encryptedPrivateKey;
|
key.PrivateKeyEncrypted = encryptedPrivateKey;
|
||||||
|
|
||||||
// should be able to return original without calling decrypt
|
// should be able to return original without calling decrypt
|
||||||
Assert.Equal(privateKey, realm.GetPrivateKey(encryptionService));
|
Assert.Equal(privateKey, key.GetPrivateKey(encryptionService));
|
||||||
Assert.Equal(encryptedPrivateKey, realm.PrivateKeyEncrypted);
|
Assert.Equal(encryptedPrivateKey, key.PrivateKeyEncrypted);
|
||||||
|
|
||||||
encryptionService.Received(1).Decrypt(encryptedPrivateKey);
|
encryptionService.Received(1).Decrypt(encryptedPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ public class Db(
|
||||||
: DbContext
|
: DbContext
|
||||||
{
|
{
|
||||||
public virtual DbSet<Realm> Realms { get; set; }
|
public virtual DbSet<Realm> Realms { get; set; }
|
||||||
|
public virtual DbSet<Key> Keys { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
45
IdentityShroud.Core/Model/Key.cs
Normal file
45
IdentityShroud.Core/Model/Key.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
|
|
||||||
|
[Table("key")]
|
||||||
|
public class Key
|
||||||
|
{
|
||||||
|
private byte[] _privateKeyDecrypted = [];
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? DeactivatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Key with highest priority will be used. While there is not really a use case for this I know some users
|
||||||
|
/// are more comfortable replacing keys by using priority then directly deactivating the old key.
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; } = 10;
|
||||||
|
|
||||||
|
public byte[] PrivateKeyEncrypted
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
field = value;
|
||||||
|
_privateKeyDecrypted = [];
|
||||||
|
}
|
||||||
|
} = [];
|
||||||
|
|
||||||
|
public byte[] GetPrivateKey(IEncryptionService encryptionService)
|
||||||
|
{
|
||||||
|
if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
|
||||||
|
_privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
|
||||||
|
return _privateKeyDecrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPrivateKey(IEncryptionService encryptionService, byte[] privateKey)
|
||||||
|
{
|
||||||
|
PrivateKeyEncrypted = encryptionService.Encrypt(privateKey);
|
||||||
|
_privateKeyDecrypted = privateKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using IdentityShroud.Core.Contracts;
|
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Model;
|
namespace IdentityShroud.Core.Model;
|
||||||
|
|
||||||
[Table("realm")]
|
[Table("realm")]
|
||||||
public class Realm
|
public class Realm
|
||||||
{
|
{
|
||||||
private byte[] _privateKeyDecrypted = [];
|
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -20,26 +18,5 @@ public class Realm
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public List<Client> Clients { get; init; } = [];
|
public List<Client> Clients { get; init; } = [];
|
||||||
|
|
||||||
public byte[] PrivateKeyEncrypted
|
public List<Key> Keys { get; init; } = [];
|
||||||
{
|
|
||||||
get;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
field = value;
|
|
||||||
_privateKeyDecrypted = [];
|
|
||||||
}
|
|
||||||
} = [];
|
|
||||||
|
|
||||||
public byte[] GetPrivateKey(IEncryptionService encryptionService)
|
|
||||||
{
|
|
||||||
if (_privateKeyDecrypted.Length == 0 && PrivateKeyEncrypted.Length > 0)
|
|
||||||
_privateKeyDecrypted = encryptionService.Decrypt(PrivateKeyEncrypted);
|
|
||||||
return _privateKeyDecrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPrivateKey(IEncryptionService encryptionService, byte[] privateKey)
|
|
||||||
{
|
|
||||||
PrivateKeyEncrypted = encryptionService.Encrypt(privateKey);
|
|
||||||
_privateKeyDecrypted = privateKey;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
using IdentityShroud.Core.Messages.Realm;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Services;
|
namespace IdentityShroud.Core.Services;
|
||||||
|
|
||||||
public interface IRealmService
|
public interface IRealmService
|
||||||
{
|
{
|
||||||
|
Task<Realm?> FindBySlug(string slug, CancellationToken ct = default);
|
||||||
|
|
||||||
Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default);
|
Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,11 @@ public class RealmService(
|
||||||
Db db,
|
Db db,
|
||||||
IEncryptionService encryptionService) : IRealmService
|
IEncryptionService encryptionService) : IRealmService
|
||||||
{
|
{
|
||||||
|
public Task<Realm?> FindBySlug(string slug, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default)
|
public async Task<Result<RealmCreateResponse>> Create(RealmCreateRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
Realm realm = new()
|
Realm realm = new()
|
||||||
|
|
@ -19,10 +24,10 @@ public class RealmService(
|
||||||
Id = request.Id ?? Guid.CreateVersion7(),
|
Id = request.Id ?? Guid.CreateVersion7(),
|
||||||
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
|
Slug = request.Slug ?? SlugHelper.GenerateSlug(request.Name),
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
|
Keys = [ CreateKey() ],
|
||||||
};
|
};
|
||||||
|
|
||||||
using RSA rsa = RSA.Create(2048);
|
|
||||||
realm.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
|
|
||||||
|
|
||||||
db.Add(realm);
|
db.Add(realm);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
@ -30,4 +35,17 @@ public class RealmService(
|
||||||
return new RealmCreateResponse(
|
return new RealmCreateResponse(
|
||||||
realm.Id, realm.Slug, realm.Name);
|
realm.Id, realm.Slug, realm.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Key CreateKey()
|
||||||
|
{
|
||||||
|
using RSA rsa = RSA.Create(2048);
|
||||||
|
|
||||||
|
Key key = new()
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
};
|
||||||
|
key.SetPrivateKey(encryptionService, rsa.ExportPkcs8PrivateKey());
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using IdentityShroud.TestUtils.Asserts;
|
||||||
|
using Xunit.Sdk;
|
||||||
|
|
||||||
|
namespace IdentityShroud.TestUtils.Tests.Asserts;
|
||||||
|
|
||||||
|
public class JsonObjectAssertTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("foo", new string[] { "foo" })]
|
||||||
|
[InlineData("foo.bar", new string[] { "foo", "bar" })]
|
||||||
|
[InlineData("foo[1].bar", new string[] { "foo", "1", "bar" })]
|
||||||
|
public void ParsePath(string path, string[] expected)
|
||||||
|
{
|
||||||
|
var result = JsonObjectAssert.ParsePath(path);
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateToPath_Success()
|
||||||
|
{
|
||||||
|
JsonObject foo = new();
|
||||||
|
foo["bar"] = 1;
|
||||||
|
JsonObject obj = new();
|
||||||
|
obj["foo"] = foo;
|
||||||
|
|
||||||
|
JsonNode? node = JsonObjectAssert.NavigateToPath(obj, ["foo", "bar"]);
|
||||||
|
Assert.NotNull(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateToPath_PathDoesNotExist()
|
||||||
|
{
|
||||||
|
JsonObject obj = new();
|
||||||
|
Assert.Throws<XunitException>(
|
||||||
|
() => JsonObjectAssert.NavigateToPath(obj, ["test"]),
|
||||||
|
ex => ex.Message.StartsWith("Path 'test' does not exist") ? null : ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateToPath_MemberOfNullObject()
|
||||||
|
{
|
||||||
|
JsonObject obj = new();
|
||||||
|
obj["foo"] = null;
|
||||||
|
|
||||||
|
Assert.Throws<XunitException>(
|
||||||
|
() => JsonObjectAssert.NavigateToPath(obj, ["foo", "bar"]),
|
||||||
|
ex => ex.Message.StartsWith("Path 'foo.bar' does not exist") ? null : ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equal_WrongType()
|
||||||
|
{
|
||||||
|
JsonObject obj = new();
|
||||||
|
obj["test"] = new JsonObject();
|
||||||
|
|
||||||
|
Assert.Throws<XunitException>(
|
||||||
|
() => JsonObjectAssert.Equal("str", obj, ["test"]),
|
||||||
|
ex => ex.Message.StartsWith("Type mismatch") ? null : ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Equal_Match()
|
||||||
|
{
|
||||||
|
JsonObject obj = new();
|
||||||
|
obj["test"] = "str";
|
||||||
|
|
||||||
|
JsonObjectAssert.Equal("str", obj, ["test"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4"/>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
|
||||||
|
<PackageReference Include="NSubstitute" Version="5.3.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>
|
||||||
|
<ProjectReference Include="..\IdentityShroud.TestUtils\IdentityShroud.TestUtils.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
364
IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs
Normal file
364
IdentityShroud.TestUtils/Asserts/JsonObjectAssert.cs
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace IdentityShroud.TestUtils.Asserts;
|
||||||
|
|
||||||
|
public static class JsonObjectAssert
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a path string that may contain array indices (e.g., "items[0].name") into individual segments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path string with optional array indices</param>
|
||||||
|
/// <returns>Array of path segments where array indices are separate segments</returns>
|
||||||
|
public static string[] ParsePath(string path)
|
||||||
|
{
|
||||||
|
var segments = new List<string>();
|
||||||
|
var parts = path.Split('.');
|
||||||
|
|
||||||
|
foreach (var part in parts)
|
||||||
|
{
|
||||||
|
// Check if the part contains array indexing like "items[0]"
|
||||||
|
var match = Regex.Match(part, @"^(.+?)\[(\d+)\]$");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
// Add the property name
|
||||||
|
segments.Add(match.Groups[1].Value);
|
||||||
|
// Add the array index
|
||||||
|
segments.Add(match.Groups[2].Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
segments.Add(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Navigates to a JsonNode at the specified path and returns it.
|
||||||
|
/// Throws XunitException if the path doesn't exist or is invalid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The root JsonObject to navigate from</param>
|
||||||
|
/// <param name="pathArray">The path segments to navigate</param>
|
||||||
|
/// <returns>The JsonNode at the specified path (can be null if the value is null)</returns>
|
||||||
|
public static JsonNode? NavigateToPath(JsonObject jsonObject, string[] pathArray)
|
||||||
|
{
|
||||||
|
if (pathArray.Length == 0)
|
||||||
|
throw new ArgumentException("Path cannot be empty");
|
||||||
|
|
||||||
|
JsonNode? current = jsonObject;
|
||||||
|
string currentPath = "";
|
||||||
|
|
||||||
|
foreach (var segment in pathArray)
|
||||||
|
{
|
||||||
|
currentPath = string.IsNullOrEmpty(currentPath) ? segment : $"{currentPath}.{segment}";
|
||||||
|
|
||||||
|
if (current == null)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{currentPath}' does not exist - parent node is null");
|
||||||
|
|
||||||
|
if (current is JsonObject obj)
|
||||||
|
{
|
||||||
|
if (!obj.ContainsKey(segment))
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{currentPath}' does not exist - property '{segment}' not found");
|
||||||
|
|
||||||
|
current = obj[segment];
|
||||||
|
}
|
||||||
|
else if (current is JsonArray arr && int.TryParse(segment, out int index))
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= arr.Count)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{currentPath}' does not exist - array index {index} out of bounds (array length: {arr.Count})");
|
||||||
|
|
||||||
|
current = arr[index];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{currentPath}' is invalid - cannot navigate through non-object/non-array node at '{segment}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a JsonObject contains the expected value at the specified path.
|
||||||
|
/// Validates that the path exists, field types match, and values are equal.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of the value</typeparam>
|
||||||
|
/// <param name="expected">The expected value</param>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the field as an enumerable of property names</param>
|
||||||
|
public static void Equal<T>(T expected, JsonObject jsonObject, IEnumerable<string> path)
|
||||||
|
{
|
||||||
|
var pathArray = path.ToArray();
|
||||||
|
var current = NavigateToPath(jsonObject, pathArray);
|
||||||
|
|
||||||
|
if (current == null)
|
||||||
|
{
|
||||||
|
if (expected != null)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Expected value '{expected}' at path '{string.Join(".", pathArray)}', but found null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type and value validation
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T? actualValue = current.GetValue<T>();
|
||||||
|
Assert.Equal(expected, actualValue);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Type mismatch at path '{string.Join(".", pathArray)}': cannot convert JsonNode to {typeof(T).Name}. {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (FormatException ex)
|
||||||
|
{
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Format error at path '{string.Join(".", pathArray)}': cannot convert value to {typeof(T).Name}. {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a JsonObject contains the expected value at the specified path.
|
||||||
|
/// Validates that the path exists, field types match, and values are equal.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of the value</typeparam>
|
||||||
|
/// <param name="expected">The expected value</param>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the field as dot-separated string with optional array indices (e.g., "user.addresses[0].city")</param>
|
||||||
|
public static void Equal<T>(T expected, JsonObject jsonObject, string path)
|
||||||
|
{
|
||||||
|
Equal(expected, jsonObject, ParsePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a path exists in the JsonObject without validating the value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to check for existence</param>
|
||||||
|
public static void PathExists(JsonObject jsonObject, IEnumerable<string> path)
|
||||||
|
{
|
||||||
|
var pathArray = path.ToArray();
|
||||||
|
NavigateToPath(jsonObject, pathArray);
|
||||||
|
// If NavigateToPath doesn't throw, the path exists
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a path exists in the JsonObject without validating the value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to check for existence as dot-separated string with optional array indices</param>
|
||||||
|
public static void PathExists(JsonObject jsonObject, string path)
|
||||||
|
{
|
||||||
|
PathExists(jsonObject, ParsePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a JsonArray at the specified path has the expected count.
|
||||||
|
/// Validates that the path exists, is a JsonArray, and has the expected number of elements.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expectedCount">The expected number of elements in the array</param>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array as an enumerable of property names</param>
|
||||||
|
public static void Count(int expectedCount, JsonObject jsonObject, IEnumerable<string> path)
|
||||||
|
{
|
||||||
|
var pathArray = path.ToArray();
|
||||||
|
var current = NavigateToPath(jsonObject, pathArray);
|
||||||
|
var pathString = string.Join(".", pathArray);
|
||||||
|
|
||||||
|
if (current == null)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{pathString}' contains null - cannot verify count on null value");
|
||||||
|
|
||||||
|
if (current is not JsonArray array)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{pathString}' does not contain a JsonArray - found {current.GetType().Name} instead");
|
||||||
|
|
||||||
|
if (array.Count != expectedCount)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Expected array at path '{pathString}' to have {expectedCount} element(s), but found {array.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a JsonArray at the specified path has the expected count.
|
||||||
|
/// Validates that the path exists, is a JsonArray, and has the expected number of elements.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="expectedCount">The expected number of elements in the array</param>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")</param>
|
||||||
|
public static void Count(int expectedCount, JsonObject jsonObject, string path)
|
||||||
|
{
|
||||||
|
Count(expectedCount, jsonObject, ParsePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
|
||||||
|
/// Validates that the path exists and is a JsonArray.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to navigate</param>
|
||||||
|
/// <param name="path">The path to the array as an enumerable of property names</param>
|
||||||
|
/// <returns>The JsonArray at the specified path</returns>
|
||||||
|
public static JsonArray GetArray(JsonObject jsonObject, IEnumerable<string> path)
|
||||||
|
{
|
||||||
|
var pathArray = path.ToArray();
|
||||||
|
var current = NavigateToPath(jsonObject, pathArray);
|
||||||
|
var pathString = string.Join(".", pathArray);
|
||||||
|
|
||||||
|
if (current == null)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{pathString}' contains null - expected a JsonArray");
|
||||||
|
|
||||||
|
if (current is not JsonArray array)
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Path '{pathString}' does not contain a JsonArray - found {current.GetType().Name} instead");
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a JsonArray at the specified path for performing custom assertions on its elements.
|
||||||
|
/// Validates that the path exists and is a JsonArray.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to navigate</param>
|
||||||
|
/// <param name="path">The path to the array as dot-separated string with optional array indices (e.g., "user.addresses")</param>
|
||||||
|
/// <returns>The JsonArray at the specified path</returns>
|
||||||
|
public static JsonArray GetArray(JsonObject jsonObject, string path)
|
||||||
|
{
|
||||||
|
return GetArray(jsonObject, ParsePath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array</param>
|
||||||
|
/// <param name="predicate">The predicate to test each element against</param>
|
||||||
|
public static void All(JsonObject jsonObject, IEnumerable<string> path, Func<JsonNode?, bool> predicate)
|
||||||
|
{
|
||||||
|
var array = GetArray(jsonObject, path);
|
||||||
|
var pathString = string.Join(".", path);
|
||||||
|
|
||||||
|
for (int i = 0; i < array.Count; i++)
|
||||||
|
{
|
||||||
|
if (!predicate(array[i]))
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Predicate failed for element at index {i} in array at path '{pathString}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that all elements in a JsonArray at the specified path satisfy the given predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array as dot-separated string</param>
|
||||||
|
/// <param name="predicate">The predicate to test each element against</param>
|
||||||
|
public static void All(JsonObject jsonObject, string path, Func<JsonNode?, bool> predicate)
|
||||||
|
{
|
||||||
|
All(jsonObject, ParsePath(path), predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array</param>
|
||||||
|
/// <param name="predicate">The predicate to test each element against</param>
|
||||||
|
public static void Any(JsonObject jsonObject, IEnumerable<string> path, Func<JsonNode?, bool> predicate)
|
||||||
|
{
|
||||||
|
var array = GetArray(jsonObject, path);
|
||||||
|
var pathString = string.Join(".", path);
|
||||||
|
|
||||||
|
foreach (var element in array)
|
||||||
|
{
|
||||||
|
if (predicate(element))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"No element in array at path '{pathString}' satisfies the predicate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that at least one element in a JsonArray at the specified path satisfies the given predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array as dot-separated string</param>
|
||||||
|
/// <param name="predicate">The predicate to test each element against</param>
|
||||||
|
public static void Any(JsonObject jsonObject, string path, Func<JsonNode?, bool> predicate)
|
||||||
|
{
|
||||||
|
Any(jsonObject, ParsePath(path), predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an action on each element in a JsonArray at the specified path.
|
||||||
|
/// Useful for running custom assertions on each element.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array</param>
|
||||||
|
/// <param name="assertAction">The action to perform on each element</param>
|
||||||
|
public static void ForEach(JsonObject jsonObject, IEnumerable<string> path, Action<JsonNode?, int> assertAction)
|
||||||
|
{
|
||||||
|
var array = GetArray(jsonObject, path);
|
||||||
|
|
||||||
|
for (int i = 0; i < array.Count; i++)
|
||||||
|
{
|
||||||
|
assertAction(array[i], i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs an action on each element in a JsonArray at the specified path.
|
||||||
|
/// Useful for running custom assertions on each element.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="path">The path to the array as dot-separated string</param>
|
||||||
|
/// <param name="assertAction">The action to perform on each element (element, index)</param>
|
||||||
|
public static void ForEach(JsonObject jsonObject, string path, Action<JsonNode?, int> assertAction)
|
||||||
|
{
|
||||||
|
ForEach(jsonObject, ParsePath(path), assertAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asserts that a JsonArray at the specified path contains an element with a specific value at a property path.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of the value</typeparam>
|
||||||
|
/// <param name="jsonObject">The JsonObject to validate</param>
|
||||||
|
/// <param name="arrayPath">The path to the array</param>
|
||||||
|
/// <param name="propertyPath">The property path within each array element to check</param>
|
||||||
|
/// <param name="expectedValue">The expected value</param>
|
||||||
|
public static void Contains<T>(JsonObject jsonObject, string arrayPath, string propertyPath, T expectedValue)
|
||||||
|
{
|
||||||
|
var array = GetArray(jsonObject, arrayPath);
|
||||||
|
var propertySegments = ParsePath(propertyPath);
|
||||||
|
|
||||||
|
foreach (var element in array)
|
||||||
|
{
|
||||||
|
if (element is JsonObject elementObj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var current = NavigateToPath(elementObj, propertySegments);
|
||||||
|
if (current != null)
|
||||||
|
{
|
||||||
|
var actualValue = current.GetValue<T>();
|
||||||
|
if (EqualityComparer<T>.Default.Equals(actualValue, expectedValue))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Continue checking other elements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Xunit.Sdk.XunitException(
|
||||||
|
$"Array at path '{arrayPath}' does not contain an element with {propertyPath} = {expectedValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Tests;
|
namespace IdentityShroud.Core.Tests;
|
||||||
|
|
||||||
15
IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj
Normal file
15
IdentityShroud.TestUtils/IdentityShroud.TestUtils.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsTestProject>false</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FluentResults" Version="4.0.0" />
|
||||||
|
<PackageReference Include="xunit.v3.assert" Version="3.2.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -12,6 +12,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Migrations",
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Api.Tests", "IdentityShroud.Api.Tests\IdentityShroud.Api.Tests.csproj", "{4758FE2E-A437-44F0-B58E-09E52D67D288}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.Api.Tests", "IdentityShroud.Api.Tests\IdentityShroud.Api.Tests.csproj", "{4758FE2E-A437-44F0-B58E-09E52D67D288}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.TestUtils", "IdentityShroud.TestUtils\IdentityShroud.TestUtils.csproj", "{A8554BCC-C9B6-4D96-90AD-FE80E95441F4}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityShroud.TestUtils.Tests", "IdentityShroud.TestUtils.Tests\IdentityShroud.TestUtils.Tests.csproj", "{35D33207-27A8-43E9-A8CA-A158A1E4448C}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{980900AA-E052-498B-A41A-4F33A8678828}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -38,5 +44,19 @@ Global
|
||||||
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4758FE2E-A437-44F0-B58E-09E52D67D288}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A8554BCC-C9B6-4D96-90AD-FE80E95441F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A8554BCC-C9B6-4D96-90AD-FE80E95441F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A8554BCC-C9B6-4D96-90AD-FE80E95441F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A8554BCC-C9B6-4D96-90AD-FE80E95441F4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{35D33207-27A8-43E9-A8CA-A158A1E4448C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{35D33207-27A8-43E9-A8CA-A158A1E4448C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{35D33207-27A8-43E9-A8CA-A158A1E4448C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{4758FE2E-A437-44F0-B58E-09E52D67D288} = {980900AA-E052-498B-A41A-4F33A8678828}
|
||||||
|
{DC887623-8680-4D3B-B23A-D54F7DA91891} = {980900AA-E052-498B-A41A-4F33A8678828}
|
||||||
|
{35D33207-27A8-43E9-A8CA-A158A1E4448C} = {980900AA-E052-498B-A41A-4F33A8678828}
|
||||||
|
{A8554BCC-C9B6-4D96-90AD-FE80E95441F4} = {980900AA-E052-498B-A41A-4F33A8678828}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,14 @@
|
||||||
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7d190ab0_002D4f9d_002D4f9f_002Dad83_002Da57b539f3bbd/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=5468c5de_002Dd2de_002D4c6e_002D97d4_002Dbb5f43ed1090/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution #3" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=a4b5fea0_002D4511_002D4f66_002D888d_002Daea8a1e4c94d/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
|
<Solution />
|
||||||
|
</SessionState></s:String>
|
||||||
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=b6b17914_002D7f7b_002D403e_002Db1eb_002D2c847c515018/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
|
<Solution />
|
||||||
|
</SessionState></s:String>
|
||||||
|
|
||||||
</wpf:ResourceDictionary>
|
</wpf:ResourceDictionary>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue