5-improve-encrypted-storage (#6)
Added the use of DEK's for encryption of secrets. Both the KEK's and DEK's are stored in a way that you can have multiple key of which one is active. But the others are still available for decrypting. This allows for implementing key rotation. Co-authored-by: eelke <eelke@eelkeklein.nl> Co-authored-by: Eelke76 <31384324+Eelke76@users.noreply.github.com> Reviewed-on: #6
This commit is contained in:
parent
138f335af0
commit
07393f57fc
87 changed files with 1903 additions and 533 deletions
155
IdentityShroud.Core.Tests/Services/ClientServiceTests.cs
Normal file
155
IdentityShroud.Core.Tests/Services/ClientServiceTests.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Services;
|
||||
using IdentityShroud.Core.Tests.Fixtures;
|
||||
using IdentityShroud.TestUtils.Substitutes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
||||
public class ClientServiceTests : IClassFixture<DbFixture>
|
||||
{
|
||||
private readonly DbFixture _dbFixture;
|
||||
private readonly NullDataEncryptionService _dataEncryptionService = new();
|
||||
|
||||
private readonly IClock _clock = Substitute.For<IClock>();
|
||||
private readonly Guid _realmId = new("a1b2c3d4-0000-0000-0000-000000000001");
|
||||
|
||||
public ClientServiceTests(DbFixture dbFixture)
|
||||
{
|
||||
_dbFixture = dbFixture;
|
||||
using Db db = dbFixture.CreateDbContext();
|
||||
if (!db.Database.EnsureCreated())
|
||||
TruncateTables(db);
|
||||
EnsureRealm(db);
|
||||
}
|
||||
|
||||
private void TruncateTables(Db db)
|
||||
{
|
||||
db.Database.ExecuteSqlRaw("TRUNCATE client CASCADE;");
|
||||
db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
|
||||
}
|
||||
|
||||
private void EnsureRealm(Db db)
|
||||
{
|
||||
if (!db.Realms.Any(r => r.Id == _realmId))
|
||||
{
|
||||
db.Realms.Add(new() { Id = _realmId, Slug = "test-realm", Name = "Test Realm" });
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Create(bool allowClientCredentialsFlow)
|
||||
{
|
||||
// Setup
|
||||
DateTime now = DateTime.UtcNow;
|
||||
_clock.UtcNow().Returns(now);
|
||||
|
||||
Client val;
|
||||
await using (var db = _dbFixture.CreateDbContext())
|
||||
{
|
||||
// Act
|
||||
ClientService sut = new(db, _dataEncryptionService, _clock);
|
||||
var response = await sut.Create(
|
||||
_realmId,
|
||||
new ClientCreateRequest
|
||||
{
|
||||
ClientId = "test-client",
|
||||
Name = "Test Client",
|
||||
Description = "A test client",
|
||||
AllowClientCredentialsFlow = allowClientCredentialsFlow,
|
||||
},
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
val = ResultAssert.Success(response);
|
||||
Assert.Equal(_realmId, val.RealmId);
|
||||
Assert.Equal("test-client", val.ClientId);
|
||||
Assert.Equal("Test Client", val.Name);
|
||||
Assert.Equal("A test client", val.Description);
|
||||
Assert.Equal(allowClientCredentialsFlow, val.AllowClientCredentialsFlow);
|
||||
Assert.Equal(now, val.CreatedAt);
|
||||
}
|
||||
|
||||
await using (var db = _dbFixture.CreateDbContext())
|
||||
{
|
||||
var dbRecord = await db.Clients
|
||||
.Include(e => e.Secrets)
|
||||
.SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
|
||||
|
||||
if (allowClientCredentialsFlow)
|
||||
Assert.Single(dbRecord.Secrets);
|
||||
else
|
||||
Assert.Empty(dbRecord.Secrets);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("existing-client", true)]
|
||||
[InlineData("missing-client", false)]
|
||||
public async Task GetByClientId(string clientId, bool shouldFind)
|
||||
{
|
||||
// Setup
|
||||
_clock.UtcNow().Returns(DateTime.UtcNow);
|
||||
await using (var setupContext = _dbFixture.CreateDbContext())
|
||||
{
|
||||
setupContext.Clients.Add(new()
|
||||
{
|
||||
RealmId = _realmId,
|
||||
ClientId = "existing-client",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
await using var actContext = _dbFixture.CreateDbContext();
|
||||
// Act
|
||||
ClientService sut = new(actContext, _dataEncryptionService, _clock);
|
||||
Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
if (shouldFind)
|
||||
Assert.NotNull(result);
|
||||
else
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task FindById(bool shouldFind)
|
||||
{
|
||||
// Setup
|
||||
_clock.UtcNow().Returns(DateTime.UtcNow);
|
||||
int existingId;
|
||||
await using (var setupContext = _dbFixture.CreateDbContext())
|
||||
{
|
||||
Client client = new()
|
||||
{
|
||||
RealmId = _realmId,
|
||||
ClientId = "find-by-id-client",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
setupContext.Clients.Add(client);
|
||||
await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
existingId = client.Id;
|
||||
}
|
||||
|
||||
int searchId = shouldFind ? existingId : existingId + 9999;
|
||||
|
||||
await using var actContext = _dbFixture.CreateDbContext();
|
||||
// Act
|
||||
ClientService sut = new(actContext, _dataEncryptionService, _clock);
|
||||
Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
if (shouldFind)
|
||||
Assert.NotNull(result);
|
||||
else
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Services;
|
||||
using IdentityShroud.TestUtils.Substitutes;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
||||
public class DataEncryptionServiceTests
|
||||
{
|
||||
private readonly IRealmContext _realmContext = Substitute.For<IRealmContext>();
|
||||
private readonly IDekEncryptionService _dekCryptor = new NullDekEncryptionService();// Substitute.For<IDekEncryptionService>();
|
||||
|
||||
private readonly DekId _activeDekId = DekId.NewId();
|
||||
private readonly DekId _secondDekId = DekId.NewId();
|
||||
private DataEncryptionService CreateSut()
|
||||
=> new(_realmContext, _dekCryptor);
|
||||
|
||||
[Fact]
|
||||
public void Encrypt_UsesActiveKey()
|
||||
{
|
||||
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([
|
||||
CreateRealmDek(_secondDekId, false),
|
||||
CreateRealmDek(_activeDekId, true),
|
||||
]);
|
||||
|
||||
var cipher = CreateSut().Encrypt("Hello"u8);
|
||||
|
||||
Assert.Equal(_activeDekId, cipher.DekId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Decrypt_UsesCorrectKey()
|
||||
{
|
||||
var first = CreateRealmDek(_activeDekId, true);
|
||||
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([ first ]);
|
||||
|
||||
var sut = CreateSut();
|
||||
var cipher = sut.Encrypt("Hello"u8);
|
||||
|
||||
// Deactivate original key
|
||||
first.Active = false;
|
||||
// Make new active
|
||||
var second = CreateRealmDek(_secondDekId, true);
|
||||
// Return both
|
||||
_realmContext.GetDeks(Arg.Any<CancellationToken>()).Returns([ first, second ]);
|
||||
|
||||
|
||||
var decoded = sut.Decrypt(cipher);
|
||||
|
||||
Assert.Equal("Hello"u8, decoded);
|
||||
}
|
||||
|
||||
private RealmDek CreateRealmDek(DekId id, bool active)
|
||||
=> new()
|
||||
{
|
||||
Id = id,
|
||||
Active = active,
|
||||
Algorithm = "AES",
|
||||
KeyData = new(KekId.NewId(), RandomNumberGenerator.GetBytes(32)),
|
||||
RealmId = default,
|
||||
};
|
||||
}
|
||||
123
IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs
Normal file
123
IdentityShroud.Core.Tests/Services/DekEncryptionServiceTests.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
||||
public class DekEncryptionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundtripWorks()
|
||||
{
|
||||
// Note this code will tend to only test the latest verion.
|
||||
|
||||
// setup
|
||||
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
|
||||
var secretProvider = Substitute.For<ISecretProvider>();
|
||||
KeyEncryptionKey[] keys =
|
||||
[
|
||||
new KeyEncryptionKey(KekId.NewId(), true, "AES", keyValue)
|
||||
];
|
||||
secretProvider.GetKeys("master").Returns(keys);
|
||||
|
||||
|
||||
ReadOnlySpan<byte> input = "Hello, World!"u8;
|
||||
|
||||
// act
|
||||
DekEncryptionService sut = new(secretProvider);
|
||||
EncryptedDek cipher = sut.Encrypt(input.ToArray());
|
||||
byte[] result = sut.Decrypt(cipher);
|
||||
|
||||
// verify
|
||||
Assert.Equal(input, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetectsCorruptInput()
|
||||
{
|
||||
// When introducing a new version we need version specific tests to
|
||||
// make sure decoding of legacy data still works.
|
||||
KekId kid = KekId.NewId();
|
||||
// setup
|
||||
byte[] cipher = // NOTE INCORRECT CIPHER DO NOT USE IN OTHER TESTS
|
||||
[
|
||||
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
|
||||
193, 75, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
|
||||
];
|
||||
EncryptedDek secret = new(kid, cipher);
|
||||
|
||||
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
|
||||
var secretProvider = Substitute.For<ISecretProvider>();
|
||||
KeyEncryptionKey[] keys =
|
||||
[
|
||||
new KeyEncryptionKey(kid, true, "AES", keyValue)
|
||||
];
|
||||
secretProvider.GetKeys("master").Returns(keys);
|
||||
|
||||
// act
|
||||
DekEncryptionService sut = new(secretProvider);
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => sut.Decrypt(secret),
|
||||
ex => ex.Message.Contains("Decryption failed") ? null : "Expected Decryption failed in message");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeSelectsRightKey()
|
||||
{
|
||||
// The key is marked inactive also it is the second key
|
||||
|
||||
// setup
|
||||
KekId kid1 = KekId.NewId();
|
||||
KekId kid2 = KekId.NewId();
|
||||
|
||||
byte[] cipher =
|
||||
[
|
||||
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
|
||||
193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
|
||||
];
|
||||
EncryptedDek secret = new(kid1, cipher);
|
||||
|
||||
byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
|
||||
byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
|
||||
var secretProvider = Substitute.For<ISecretProvider>();
|
||||
KeyEncryptionKey[] keys =
|
||||
[
|
||||
new KeyEncryptionKey(kid2, true, "AES", keyValue2),
|
||||
new KeyEncryptionKey(kid1, false, "AES", keyValue1),
|
||||
];
|
||||
secretProvider.GetKeys("master").Returns(keys);
|
||||
|
||||
// act
|
||||
DekEncryptionService sut = new(secretProvider);
|
||||
byte[] result = sut.Decrypt(secret);
|
||||
|
||||
// verify
|
||||
Assert.Equal("Hello, World!"u8, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptionUsesActiveKey()
|
||||
{
|
||||
// setup
|
||||
KekId kid1 = KekId.NewId();
|
||||
KekId kid2 = KekId.NewId();
|
||||
|
||||
byte[] keyValue1 = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
|
||||
byte[] keyValue2 = Convert.FromBase64String("Dat1RwRvuLX3wdKMMP4NwHdBl8tJJsKfp01qikyo8aw=");
|
||||
var secretProvider = Substitute.For<ISecretProvider>();
|
||||
KeyEncryptionKey[] keys =
|
||||
[
|
||||
new KeyEncryptionKey(kid1, false, "AES", keyValue1),
|
||||
new KeyEncryptionKey(kid2, true, "AES", keyValue2),
|
||||
];
|
||||
secretProvider.GetKeys("master").Returns(keys);
|
||||
|
||||
ReadOnlySpan<byte> input = "Hello, World!"u8;
|
||||
// act
|
||||
DekEncryptionService sut = new(secretProvider);
|
||||
EncryptedDek cipher = sut.Encrypt(input.ToArray());
|
||||
|
||||
// Verify
|
||||
Assert.Equal(kid2, cipher.KekId);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
||||
public class EncryptionServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void RoundtripWorks()
|
||||
{
|
||||
// setup
|
||||
string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
var secretProvider = Substitute.For<ISecretProvider>();
|
||||
secretProvider.GetSecret("Master").Returns(key);
|
||||
|
||||
EncryptionService sut = new(secretProvider);
|
||||
byte[] input = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
// act
|
||||
var cipher = sut.Encrypt(input);
|
||||
var result = sut.Decrypt(cipher);
|
||||
|
||||
Assert.Equal(input, result);
|
||||
}
|
||||
}
|
||||
30
IdentityShroud.Core.Tests/Services/EncryptionTests.cs
Normal file
30
IdentityShroud.Core.Tests/Services/EncryptionTests.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
||||
public class EncryptionTests
|
||||
{
|
||||
[Fact]
|
||||
public void DecodeV1_Success()
|
||||
{
|
||||
// When introducing a new version we need version specific tests to
|
||||
// make sure decoding of legacy data still works.
|
||||
|
||||
// setup
|
||||
byte[] cipher =
|
||||
[
|
||||
1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
|
||||
193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
|
||||
];
|
||||
byte[] keyValue = Convert.FromBase64String("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
|
||||
|
||||
// act
|
||||
byte[] result = Encryption.Decrypt(cipher, keyValue);
|
||||
|
||||
// verify
|
||||
Assert.Equal("Hello, World!"u8, result);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Model;
|
||||
using IdentityShroud.Core.Security;
|
||||
using IdentityShroud.Core.Security.Keys;
|
||||
using IdentityShroud.Core.Services;
|
||||
using IdentityShroud.Core.Tests.Fixtures;
|
||||
using IdentityShroud.TestUtils.Substitutes;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace IdentityShroud.Core.Tests.Services;
|
||||
|
|
@ -9,7 +11,7 @@ namespace IdentityShroud.Core.Tests.Services;
|
|||
public class RealmServiceTests : IClassFixture<DbFixture>
|
||||
{
|
||||
private readonly DbFixture _dbFixture;
|
||||
private readonly IEncryptionService _encryptionService = EncryptionServiceSubstitute.CreatePassthrough();
|
||||
private readonly IKeyService _keyService = Substitute.For<IKeyService>();
|
||||
|
||||
public RealmServiceTests(DbFixture dbFixture)
|
||||
{
|
||||
|
|
@ -34,25 +36,43 @@ public class RealmServiceTests : IClassFixture<DbFixture>
|
|||
if (idString is not null)
|
||||
realmId = new(idString);
|
||||
|
||||
using Db db = _dbFixture.CreateDbContext();
|
||||
RealmService sut = new(db, _encryptionService);
|
||||
// Act
|
||||
|
||||
var response = await sut.Create(
|
||||
new(realmId, "slug", "New realm"),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
RealmCreateResponse val = ResultAssert.Success(response);
|
||||
if (realmId.HasValue)
|
||||
Assert.Equal(realmId, val.Id);
|
||||
else
|
||||
Assert.NotEqual(Guid.Empty, val.Id);
|
||||
|
||||
Assert.Equal("slug", val.Slug);
|
||||
Assert.Equal("New realm", val.Name);
|
||||
|
||||
// TODO verify data has been stored!
|
||||
RealmCreateResponse? val;
|
||||
await using (var db = _dbFixture.CreateDbContext())
|
||||
{
|
||||
_keyService.CreateKey(Arg.Any<KeyPolicy>())
|
||||
.Returns(new RealmKey()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
KeyType = "TST",
|
||||
Key = new(KekId.NewId(), [21]),
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
// Act
|
||||
RealmService sut = new(db, _keyService);
|
||||
var response = await sut.Create(
|
||||
new(realmId, "slug", "New realm"),
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
val = ResultAssert.Success(response);
|
||||
if (realmId.HasValue)
|
||||
Assert.Equal(realmId, val.Id);
|
||||
else
|
||||
Assert.NotEqual(Guid.Empty, val.Id);
|
||||
|
||||
Assert.Equal("slug", val.Slug);
|
||||
Assert.Equal("New realm", val.Name);
|
||||
|
||||
_keyService.Received().CreateKey(Arg.Any<KeyPolicy>());
|
||||
}
|
||||
|
||||
await using (var db = _dbFixture.CreateDbContext())
|
||||
{
|
||||
var dbRecord = await db.Realms
|
||||
.Include(e => e.Keys)
|
||||
.SingleAsync(e => e.Id == val.Id, TestContext.Current.CancellationToken);
|
||||
Assert.Equal("TST", dbRecord.Keys[0].KeyType);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -60,7 +80,7 @@ public class RealmServiceTests : IClassFixture<DbFixture>
|
|||
[InlineData("foo", "Foo")]
|
||||
public async Task FindBySlug(string slug, string? name)
|
||||
{
|
||||
using (var setupContext = _dbFixture.CreateDbContext())
|
||||
await using (var setupContext = _dbFixture.CreateDbContext())
|
||||
{
|
||||
setupContext.Realms.Add(new()
|
||||
{
|
||||
|
|
@ -76,11 +96,48 @@ public class RealmServiceTests : IClassFixture<DbFixture>
|
|||
await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
using Db actContext = _dbFixture.CreateDbContext();
|
||||
RealmService sut = new(actContext, _encryptionService);
|
||||
await using var actContext = _dbFixture.CreateDbContext();
|
||||
// Act
|
||||
RealmService sut = new(actContext, _keyService);
|
||||
var result = await sut.FindBySlug(slug, TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
Assert.Equal(name, result?.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("b0423bba-2411-497b-a5b6-c5adf404b862", true)]
|
||||
[InlineData("65ac9dba-6d43-4fa4-b57f-133ed639fbcb", false)]
|
||||
public async Task FindById(string idString, bool shouldFind)
|
||||
{
|
||||
Guid id = new(idString);
|
||||
await using (var setupContext = _dbFixture.CreateDbContext())
|
||||
{
|
||||
setupContext.Realms.Add(new()
|
||||
{
|
||||
Id = new("b0423bba-2411-497b-a5b6-c5adf404b862"),
|
||||
Slug = "foo",
|
||||
Name = "Foo",
|
||||
});
|
||||
setupContext.Realms.Add(new()
|
||||
{
|
||||
Id = new("d4ffc7d0-7b2c-4f02-82b9-a74610435b0d"),
|
||||
Slug = "bar",
|
||||
Name = "Bar",
|
||||
});
|
||||
|
||||
await setupContext.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
await using var actContext = _dbFixture.CreateDbContext();
|
||||
// Act
|
||||
RealmService sut = new(actContext, _keyService);
|
||||
Realm? result = await sut.FindById(id, TestContext.Current.CancellationToken);
|
||||
|
||||
// Verify
|
||||
if (shouldFind)
|
||||
Assert.NotNull(result);
|
||||
else
|
||||
Assert.Null(result);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue