tests: add mucho tests

This commit is contained in:
Stevan Freeborn
2026-02-06 06:48:34 -06:00
parent e93fed6e67
commit 5772e81110
26 changed files with 1983 additions and 18 deletions
+15
View File
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Infra", "src\Fisca
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Infra.Tests", "tests\FiscalOS.Infra.Tests\FiscalOS.Infra.Tests.csproj", "{48E0737E-0200-468E-ADA7-75893935DBC0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FiscalOS.Core.Tests", "tests\FiscalOS.Core.Tests\FiscalOS.Core.Tests.csproj", "{2D9C02A9-C704-4275-88E9-1BBD3AC12700}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -101,6 +103,18 @@ Global
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x64.Build.0 = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x86.ActiveCfg = Release|Any CPU
{48E0737E-0200-468E-ADA7-75893935DBC0}.Release|x86.Build.0 = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|x64.ActiveCfg = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|x64.Build.0 = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|x86.ActiveCfg = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Debug|x86.Build.0 = Debug|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|Any CPU.Build.0 = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|x64.ActiveCfg = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|x64.Build.0 = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|x86.ActiveCfg = Release|Any CPU
{2D9C02A9-C704-4275-88E9-1BBD3AC12700}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -112,5 +126,6 @@ Global
{612496CD-1F25-4804-8C52-49E62D01FFDC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{DAB42870-02A2-4ECB-B07F-6815C52D2435} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{48E0737E-0200-468E-ADA7-75893935DBC0} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{2D9C02A9-C704-4275-88E9-1BBD3AC12700} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
+1
View File
@@ -1,5 +1,6 @@
[*.cs]
dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA1062.severity = none
dotnet_diagnostic.CA2007.severity = none
@@ -13,7 +13,6 @@
<PackageReference Include="xunit.v3.mtp-v2" />
</ItemGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
@@ -16,6 +16,8 @@ public class TestApi : WebApplicationFactory<Program>
}));
c.AddSingleton(Options.Create(JwtTokenBuilder.DefaultJwtOptions));
c.AddSingleton<IKeyRing>(TestKeyRing.From);
});
}
}
@@ -0,0 +1,31 @@
namespace FiscalOS.API.Tests.Infra;
internal sealed class TestKeyRing : IKeyRing
{
private readonly string _key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
private TestKeyRing()
{
}
public static TestKeyRing From(IServiceProvider serviceProvider)
{
return new();
}
public KeyRingEntry GetKey(string keyId)
{
return KeyRingEntry.From(keyId, _key);
}
public KeyRingEntry GetPrimaryKey()
{
return KeyRingEntry.From("primary-key-id", _key);
}
public Task<KeyRingEntry> SaveKeyAsync(string key)
{
throw new NotImplementedException();
}
}
@@ -39,8 +39,10 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
await ExecuteDbContextAsync(static async (context, sp) =>
{
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var encryptor = sp.GetRequiredService<IEncryptor>();
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1")));
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey));
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
});
@@ -62,8 +64,10 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
await ExecuteDbContextAsync(static async (context, sp) =>
{
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var encryptor = sp.GetRequiredService<IEncryptor>();
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1")));
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey));
await context.SaveChangesAsync(TestContext.Current.CancellationToken);
});
@@ -35,9 +35,12 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
{
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
var encryptor = sp.GetRequiredService<IEncryptor>();
var user1 = User.From("User1", passwordHasher.Hash("@Password1"));
var user2 = User.From("User2", passwordHasher.Hash("@Password2"));
var user1EncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user2EncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user1 = User.From("User1", passwordHasher.Hash("@Password1"), user1EncryptionKey);
var user2 = User.From("User2", passwordHasher.Hash("@Password2"), user2EncryptionKey);
var refreshToken1 = tokenGenerator.GenerateRefreshToken(user2);
var refreshToken2 = tokenGenerator.GenerateRefreshToken(user2);
user2.AddRefreshToken(refreshToken1);
@@ -79,8 +82,10 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
{
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
var encryptor = sp.GetRequiredService<IEncryptor>();
var user = User.From("User1", passwordHasher.Hash("@Password1"));
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
refreshToken.Revoke();
@@ -113,8 +118,10 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
var timeProvider = sp.GetRequiredService<TimeProvider>();
var encryptor = sp.GetRequiredService<IEncryptor>();
var user = User.From("User1", passwordHasher.Hash("@Password1"));
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var refreshToken = RefreshToken.From(user.Id, "expiredtoken", timeProvider.GetUtcNow().AddHours(-1));
user.AddRefreshToken(refreshToken);
@@ -147,8 +154,10 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
var timeProvider = sp.GetRequiredService<TimeProvider>();
var encryptor = sp.GetRequiredService<IEncryptor>();
var user = User.From("User1", passwordHasher.Hash("@Password1"));
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user = User.From("User1", passwordHasher.Hash("@Password1"), userEncryptionKey);
var refreshToken = tokenGenerator.GenerateRefreshToken(user);
user.AddRefreshToken(refreshToken);
+2
View File
@@ -7,10 +7,12 @@ global using System.Security.Cryptography;
global using AwesomeAssertions.Execution;
global using AwesomeAssertions.Primitives;
global using FiscalOS.API.Tests.Assertions;
global using FiscalOS.API.Tests.Infra;
global using FiscalOS.Core.Authentication;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Authentication;
global using FiscalOS.Infra.Data;
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>FiscalOS.Core.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
<PackageReference Include="Moq" />
<PackageReference Include="xunit.v3.mtp-v2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\FiscalOS.Core\FiscalOS.Core.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,36 @@
namespace FiscalOS.Core.Tests.Unit;
public class EncryptedDataKeyTests
{
[Fact]
public void From_WhenCalled_ItShouldReturnAnInstance()
{
var keyId = "key-id";
var encryptedKey = "encrypted-key";
var encryptedDataKey = EncryptedDataKey.From(keyId, encryptedKey);
encryptedDataKey.KeyIdUsed.Should().Be(keyId);
encryptedDataKey.EncryptedKey.Should().Be(encryptedKey);
}
[Fact]
public void From_WhenCalledWithNullKeyId_ItShouldThrowArgumentNullException()
{
var encryptedKey = "encrypted-key";
var creatingDataKeyWithNullKeyId = () => EncryptedDataKey.From(null!, encryptedKey);
creatingDataKeyWithNullKeyId.Should().Throw<ArgumentNullException>();
}
[Fact]
public void From_WhenCalledWithNullEncryptedKey_ItShouldThrowArgumentNullException()
{
var keyId = "key-id";
var creatingDataKeyWithNullEncryptedKey = () => EncryptedDataKey.From(keyId, null!);
creatingDataKeyWithNullEncryptedKey.Should().Throw<ArgumentNullException>();
}
}
@@ -0,0 +1,36 @@
namespace FiscalOS.Core.Tests.Unit;
public class KeyRingEntryTests
{
[Fact]
public void From_WhenCalled_ItShouldReturnAnInstance()
{
var keyId = "test-key-id";
var key = "key";
var keyRingEntry = KeyRingEntry.From(keyId, key);
keyRingEntry.KeyId.Should().Be(keyId);
keyRingEntry.Key.Should().Be(key);
}
[Fact]
public void From_WhenCalledWithNullKeyId_ItShouldThrowArgumentNullException()
{
var key = "key";
var createEntryWithNullKeyId = () => KeyRingEntry.From(null!, key);
createEntryWithNullKeyId.Should().Throw<ArgumentNullException>();
}
[Fact]
public void From_WhenCalledWithNullKey_ItShouldThrowArgumentNullException()
{
var keyId = "test-key-id";
var createEntryWithNullKey = () => KeyRingEntry.From(keyId, null!);
createEntryWithNullKey.Should().Throw<ArgumentNullException>();
}
}
@@ -0,0 +1,82 @@
namespace FiscalOS.Core.Tests.Unit;
public class RefreshTokenTests
{
[Fact]
public void From_WhenCalledWithUserId_ItShouldReturnARefreshTokenInstance()
{
var userId = Guid.NewGuid();
var token = "token";
var expiresAt = DateTime.UtcNow.AddDays(7);
var refreshToken = RefreshToken.From(userId, token, expiresAt);
refreshToken.UserId.Should().Be(userId);
refreshToken.User.Should().BeNull();
refreshToken.Token.Should().Be(token);
refreshToken.ExpiresAt.Should().Be(expiresAt);
refreshToken.Revoked.Should().BeFalse();
}
[Fact]
public void From_WhenCalledWithUser_ItShouldReturnARefreshTokenInstance()
{
var user = User.From(
"testuser",
"hashedpassword",
EncryptedDataKey.From("keyId", "encryptedKey")
);
var token = "token";
var expiresAt = DateTime.UtcNow.AddDays(7);
var refreshToken = RefreshToken.From(user, token, expiresAt);
refreshToken.UserId.Should().Be(user.Id);
refreshToken.User.Should().Be(user);
refreshToken.Token.Should().Be(token);
refreshToken.ExpiresAt.Should().Be(expiresAt);
refreshToken.Revoked.Should().BeFalse();
}
[Fact]
public void From_WhenCalledWithNullUser_ItShouldThrowArgumentNullException()
{
var token = "token";
var expiresAt = DateTime.UtcNow.AddDays(7);
var createRefreshTokenWithNullUser = () => RefreshToken.From(null!, token, expiresAt);
createRefreshTokenWithNullUser.Should().Throw<ArgumentNullException>();
}
[Fact]
public void Revoke_WhenCalled_ItShouldSetRevokedToTrue()
{
var refreshToken = RefreshToken.From(Guid.NewGuid(), "token", DateTime.UtcNow.AddDays(7));
refreshToken.Revoke();
refreshToken.Revoked.Should().BeTrue();
}
[Fact]
public void IsExpired_WhenCalledOnExpiredToken_ItShouldReturnTrue()
{
var refreshToken = RefreshToken.From(Guid.NewGuid(), "token", DateTime.UtcNow.AddDays(-1));
var isExpired = refreshToken.IsExpired(DateTimeOffset.UtcNow);
isExpired.Should().BeTrue();
}
[Fact]
public void IsExpired_WhenCalledOnNonExpiredToken_ItShouldReturnFalse()
{
var refreshToken = RefreshToken.From(Guid.NewGuid(), "token", DateTime.UtcNow.AddDays(1));
var isExpired = refreshToken.IsExpired(DateTimeOffset.UtcNow);
isExpired.Should().BeFalse();
}
}
@@ -0,0 +1,90 @@
namespace FiscalOS.Core.Tests.Unit;
public class UserTests
{
[Fact]
public void New_WhenCalled_ItShouldReturnNewUserInstance()
{
var user = User.New();
user.Should().NotBeNull();
user.Username.Should().BeEmpty();
user.HashedPassword.Should().BeEmpty();
user.EncryptionKeyId.Should().BeEmpty();
user.EncryptedDataKey.Should().BeEmpty();
user.RefreshTokens.Should().BeEmpty();
}
[Fact]
public void From_WhenCalledWithValidParameters_ItShouldReturnUserInstance()
{
var encryptedDataKey = EncryptedDataKey.From("keyId", "encryptedKey");
var user = User.From("testuser", "hashedpassword", encryptedDataKey);
user.Should().NotBeNull();
user.Username.Should().Be("testuser");
user.HashedPassword.Should().Be("hashedpassword");
user.EncryptionKeyId.Should().Be("keyId");
user.EncryptedDataKey.Should().Be("encryptedKey");
user.RefreshTokens.Should().BeEmpty();
}
[Fact]
public void From_WhenCalledWithNullParameters_ItShouldThrowArgumentNullException()
{
var encryptedDataKey = EncryptedDataKey.From("keyId", "encryptedKey");
var creatingUserWithNullUsername = () => User.From(null!, "hashedpassword", encryptedDataKey);
creatingUserWithNullUsername.Should().Throw<ArgumentNullException>();
var creatingUserWithNullHashedPassword = () => User.From("testuser", null!, encryptedDataKey);
creatingUserWithNullHashedPassword.Should().Throw<ArgumentNullException>();
var creatingUserWithNullDataKey = () => User.From("testuser", "hashedpassword", null!);
creatingUserWithNullDataKey.Should().Throw<ArgumentNullException>();
}
[Fact]
public void AddRefreshToken_WhenCalled_ItShouldAddRefreshTokenToUser()
{
var encryptedDataKey = EncryptedDataKey.From("keyId", "encryptedKey");
var user = User.From("testuser", "hashedpassword", encryptedDataKey);
var refreshToken = RefreshToken.From(user.Id, "tokenvalue", DateTimeOffset.UtcNow.AddDays(7));
user.AddRefreshToken(refreshToken);
user.RefreshTokens.Should().ContainSingle().Which.Should().Be(refreshToken);
}
[Fact]
public void AddRefreshToken_WhenCalledWithNull_ItShouldThrowArgumentNullException()
{
var user = User.New();
var addingNullRefreshToken = () => user.AddRefreshToken(null!);
addingNullRefreshToken.Should().Throw<ArgumentNullException>();
}
[Fact]
public void SetCreatedAt_WhenCalled_ItShouldSetCreatedAtProperty()
{
var user = User.New();
var createdAt = DateTimeOffset.UtcNow;
user.SetCreatedAt(createdAt);
user.CreatedAt.Should().Be(createdAt);
}
[Fact]
public void SetUpdatedAt_WhenCalled_ItShouldSetUpdatedAtProperty()
{
var user = User.New();
var updatedAt = DateTimeOffset.UtcNow;
user.SetUpdatedAt(updatedAt);
user.UpdatedAt.Should().Be(updatedAt);
}
}
+2
View File
@@ -0,0 +1,2 @@
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
@@ -0,0 +1,3 @@
{
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
}
@@ -20,9 +20,4 @@
<ProjectReference Include="..\..\src\FiscalOS.Infra\FiscalOS.Infra.csproj" />
</ItemGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="Test">
<Exec
Command="reportgenerator -reports:./TestResults/Coverage/*.xml -targetdir:./TestResults/Coverage/Report/ -reporttypes:Html_Dark" />
</Target>
</Project>
@@ -0,0 +1,27 @@
namespace FiscalOS.Infra.Tests.Mocks;
internal sealed class MockOptionsMonitor<T> : IOptionsMonitor<T> where T : new()
{
private Action<T, string>? _listener;
public T CurrentValue
{
get;
set
{
field = value;
_listener?.Invoke(field, string.Empty);
}
} = new();
public T Get(string? name)
{
return CurrentValue;
}
public IDisposable? OnChange(Action<T, string> listener)
{
_listener = listener;
return new Mock<IDisposable>().Object;
}
}
@@ -0,0 +1,74 @@
namespace FiscalOS.Infra.Tests.Unit;
public class EncryptorTests
{
private readonly Mock<IKeyRing> _mockKeyRing = new();
private readonly Encryptor _sut;
public EncryptorTests()
{
var key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
_mockKeyRing.Setup(static kr => kr.GetPrimaryKey()).Returns(KeyRingEntry.From("primary-key-id", key));
_sut = Encryptor.From(_mockKeyRing.Object);
}
[Fact]
public void GenerateKey_WhenCalled_ItShouldReturnKey()
{
var key = _sut.GenerateKey();
key.Should().NotBeNull();
}
[Fact]
public async Task GenerateEncryptedKeyAsync_WhenCalled_ItShouldReturnEncryptedKey()
{
var encryptedKey = await _sut.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
encryptedKey.Should().NotBeNull();
}
[Fact]
public async Task Encrypt_WhenCalledWithPlainText_ItShouldReturnCipherText()
{
var plainText = "Hello, World";
var cipherText = await _sut.EncryptAsync(plainText, TestContext.Current.CancellationToken);
cipherText.Should().NotBe(plainText);
}
[Fact]
public async Task Decrypt_WhenCalledWithCipherText_ItShouldReturnPlainText()
{
var plainText = "Hello, World";
var cipherText = await _sut.EncryptAsync(plainText, TestContext.Current.CancellationToken);
var decryptedData = await _sut.DecryptAsync(cipherText, TestContext.Current.CancellationToken);
decryptedData.Should().Be(plainText);
}
[Fact]
public async Task EncryptForUser_WhenCalledWithPlainText_ItShouldReturnCipherText()
{
var plainText = "Hello, World";
var userEncryptionKey = await _sut.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user = User.From("Stevan", "HashedPassword", userEncryptionKey);
var cipherText = await _sut.EncryptAsyncFor(user, plainText, TestContext.Current.CancellationToken);
cipherText.Should().NotBe(plainText);
}
[Fact]
public async Task DecryptForUser_WhenCalledWithCipherText_ItShouldReturnPlainText()
{
var plainText = "Hello, World";
var encryptedDataKey = await _sut.GenerateEncryptedKeyAsync(TestContext.Current.CancellationToken);
var user = User.From("Stevan", "HashedPassword", encryptedDataKey);
var cipherText = await _sut.EncryptAsyncFor(user, plainText, TestContext.Current.CancellationToken);
var decryptedData = await _sut.DecryptAsyncFor(user, cipherText, TestContext.Current.CancellationToken);
decryptedData.Should().Be(plainText);
}
}
@@ -0,0 +1,304 @@
namespace FiscalOS.Infra.Tests.Unit;
public class FileKeyRingOptionsTests
{
[Fact]
public void FileKeyRingOptions_WhenCreatedWithDefaults_ItShouldHaveCorrectDefaultValues()
{
var options = new FileKeyRingOptions();
options.KeysDirectoryPath.Should().Be(string.Empty);
options.PrimaryKeyId.Should().Be(string.Empty);
}
[Fact]
public void FileKeyRingOptions_WhenInitializedWithValues_ItShouldHaveCorrectValues()
{
var keysDirectoryPath = "/secure/keys";
var primaryKeyId = "primary-key-2024";
var options = new FileKeyRingOptions
{
KeysDirectoryPath = keysDirectoryPath,
PrimaryKeyId = primaryKeyId
};
options.KeysDirectoryPath.Should().Be(keysDirectoryPath);
options.PrimaryKeyId.Should().Be(primaryKeyId);
}
[Fact]
public void FileKeyRingOptions_WhenUsedAsRecord_ItShouldSupportEquality()
{
var options1 = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = "key-1"
};
var options2 = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = "key-1"
};
options1.Should().Be(options2);
}
[Fact]
public void FileKeyRingOptions_WhenCreatedWithDifferentValues_ItShouldNotBeEqual()
{
var options1 = new FileKeyRingOptions { KeysDirectoryPath = "/keys1" };
var options2 = new FileKeyRingOptions { KeysDirectoryPath = "/keys2" };
options1.Should().NotBe(options2);
}
[Fact]
public void FileKeyRingOptions_WhenInitializedWithLongPath_ItShouldStoreFullPath()
{
var longPath = "/secure/keys/directory/with/multiple/levels";
var options = new FileKeyRingOptions { KeysDirectoryPath = longPath };
options.KeysDirectoryPath.Should().Be(longPath);
}
[Fact]
public void FileKeyRingOptions_WhenInitializedWithRelativePath_ItShouldStoreRelativePath()
{
var relativePath = "./keys";
var options = new FileKeyRingOptions { KeysDirectoryPath = relativePath };
options.KeysDirectoryPath.Should().Be(relativePath);
}
[Fact]
public void FileKeyRingOptions_WhenUsedWithModification_ItShouldSupportWith()
{
var originalOptions = new FileKeyRingOptions
{
KeysDirectoryPath = "/original",
PrimaryKeyId = "original-key"
};
var modifiedOptions = originalOptions with { PrimaryKeyId = "modified-key" };
originalOptions.PrimaryKeyId.Should().Be("original-key");
modifiedOptions.PrimaryKeyId.Should().Be("modified-key");
modifiedOptions.KeysDirectoryPath.Should().Be("/original");
}
[Fact]
public void FileKeyRingOptionsSetup_WhenConstructed_ItShouldStoreConfiguration()
{
var configuration = new ConfigurationBuilder().Build();
var setup = new FileKeyRingOptionsSetup(configuration);
setup.Should().NotBeNull();
}
[Fact]
public void Configure_WhenCalledWithValidConfiguration_ItShouldBindFileKeyRingOptionsCorrectly()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", "/secure/keys" },
{ "FileKeyRingOptions:PrimaryKeyId", "primary-key-id" }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be("/secure/keys");
options.PrimaryKeyId.Should().Be("primary-key-id");
}
[Fact]
public void Configure_WhenCalledWithPartialConfiguration_ItShouldBindAvailableValuesAndKeepDefaults()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", "/keys" }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be("/keys");
options.PrimaryKeyId.Should().Be(string.Empty);
}
[Fact]
public void Configure_WhenCalledWithMissingSection_ItShouldLeaveOptionsUnchanged()
{
var configuration = new ConfigurationBuilder().Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(string.Empty);
options.PrimaryKeyId.Should().Be(string.Empty);
}
[Fact]
public void Configure_WhenCalledWithEmptyStringValues_ItShouldBindEmptyStrings()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", "" },
{ "FileKeyRingOptions:PrimaryKeyId", "" }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(string.Empty);
options.PrimaryKeyId.Should().Be(string.Empty);
}
[Fact]
public void Configure_WhenCalledWithComplexDirectoryPath_ItShouldBindFullPath()
{
var complexPath = "/var/lib/app/secure/encryption/keys/storage";
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", complexPath }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(complexPath);
}
[Fact]
public void Configure_WhenCalledWithRelativeDirectoryPath_ItShouldBindRelativePath()
{
var relativePath = "./keys";
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", relativePath }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(relativePath);
}
[Fact]
public void Configure_WhenCalledWithGuidKeyId_ItShouldBindKeyId()
{
var guidKeyId = "550e8400-e29b-41d4-a716-446655440000";
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:PrimaryKeyId", guidKeyId }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.PrimaryKeyId.Should().Be(guidKeyId);
}
[Fact]
public void Configure_WhenCalledMultipleTimes_ItShouldUpdateOptionsEachTime()
{
var configBuilder1 = new ConfigurationBuilder();
configBuilder1.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", "/keys1" }
});
var configuration1 = configBuilder1.Build();
var setup1 = new FileKeyRingOptionsSetup(configuration1);
var options = new FileKeyRingOptions();
setup1.Configure(options);
options.KeysDirectoryPath.Should().Be("/keys1");
var configBuilder2 = new ConfigurationBuilder();
configBuilder2.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", "/keys2" }
});
var configuration2 = configBuilder2.Build();
var setup2 = new FileKeyRingOptionsSetup(configuration2);
setup2.Configure(options);
options.KeysDirectoryPath.Should().Be("/keys2");
}
[Fact]
public void Configure_WhenCalledWithWindowsPath_ItShouldBindWindowsPath()
{
var windowsPath = "C:\\secure\\keys";
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", windowsPath }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(windowsPath);
}
[Fact]
public void Configure_WhenCalledWithOnlyPrimaryKeyId_ItShouldBindKeyIdAndLeavePathDefault()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:PrimaryKeyId", "my-key-id" }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.PrimaryKeyId.Should().Be("my-key-id");
options.KeysDirectoryPath.Should().Be(string.Empty);
}
[Fact]
public void Configure_WhenCalledWithWhitespaceValues_ItShouldBindWhitespace()
{
var whitespaceValue = " ";
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "FileKeyRingOptions:KeysDirectoryPath", whitespaceValue }
});
var configuration = configBuilder.Build();
var setup = new FileKeyRingOptionsSetup(configuration);
var options = new FileKeyRingOptions();
setup.Configure(options);
options.KeysDirectoryPath.Should().Be(whitespaceValue);
}
}
@@ -0,0 +1,495 @@
namespace FiscalOS.Infra.Tests.Unit;
public class FileKeyRingTests
{
private readonly MockOptionsMonitor<FileKeyRingOptions> _mockOptionsMonitor = new();
private readonly Mock<IFileSystem> _mockFileSystem = new();
[Fact]
public void From_WhenCalledWithOptionsMonitorAndFileSystem_ItShouldCreateFileKeyRing()
{
var mockPath = new Mock<IPath>();
var mockDirectory = new Mock<IDirectory>();
_mockFileSystem
.Setup(static fs => fs.Path)
.Returns(mockPath.Object);
_mockFileSystem
.Setup(static fs => fs.Directory)
.Returns(mockDirectory.Object);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
sut.Should().NotBeNull();
sut.Should().BeAssignableTo<IKeyRing>();
sut.Should().BeAssignableTo<IDisposable>();
}
[Fact]
public void From_WhenCalledWithServiceProvider_ItShouldCreateFileKeyRing()
{
var mockPath = new Mock<IPath>();
var mockDirectory = new Mock<IDirectory>();
var mockFileSystem = new Mock<IFileSystem>();
var mockServiceProvider = new Mock<IServiceProvider>();
var mockOptionsMonitor = new MockOptionsMonitor<FileKeyRingOptions>();
mockFileSystem
.Setup(static fs => fs.Path)
.Returns(mockPath.Object);
mockFileSystem
.Setup(static fs => fs.Directory)
.Returns(mockDirectory.Object);
mockServiceProvider
.Setup(static sp => sp.GetService(typeof(IOptionsMonitor<FileKeyRingOptions>)))
.Returns(mockOptionsMonitor);
mockServiceProvider
.Setup(static sp => sp.GetService(typeof(IFileSystem)))
.Returns(mockFileSystem.Object);
var sut = FileKeyRing.From(mockServiceProvider.Object);
sut.Should().NotBeNull();
sut.Should().BeAssignableTo<IKeyRing>();
}
[Fact]
public void GetKey_WhenCalledWithValidKeyId_ItShouldReturnKey()
{
var keyId = "test-key";
var keyContent = "test-key-content";
var mockPath = new Mock<IPath>();
var mockDirectory = new Mock<IDirectoryInfoFactory>();
var mockFile = new Mock<IFileInfoFactory>();
var mockDirectoryInfo = new Mock<IDirectoryInfo>();
var mockFileInfo = new Mock<IFileInfo>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = keyId
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{keyId}.key"))
.Returns(keyId);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns([$"/full/keys/{keyId}.key"]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{keyId}.key"))
.Returns(keyContent);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var result = sut.GetKey(keyId);
result.Should().NotBeNull();
result.KeyId.Should().Be(keyId);
}
[Fact]
public void GetKey_WhenCalledWithInvalidKeyId_ItShouldThrowKeyNotFoundException()
{
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = "valid-key"
};
mockPath
.Setup(p => p.GetFullPath(It.IsAny<string>(), It.IsAny<string>()))
.Returns("/full/keys");
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists(It.IsAny<string>()))
.Returns(false);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var action = () => sut.GetKey("non-existent-key");
action.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void GetPrimaryKey_WhenPrimaryKeyExists_ItShouldReturnPrimaryKey()
{
var primaryKeyId = "primary-key";
var keyContent = "primary-key-content";
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = primaryKeyId
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{primaryKeyId}.key"))
.Returns(primaryKeyId);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns([$"/full/keys/{primaryKeyId}.key"]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{primaryKeyId}.key"))
.Returns(keyContent);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var result = sut.GetPrimaryKey();
result.Should().NotBeNull();
result.KeyId.Should().Be(primaryKeyId);
}
[Fact]
public void GetPrimaryKey_WhenPrimaryKeyDoesNotExist_ItShouldThrowKeyNotFoundException()
{
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = "non-existent-primary-key"
};
mockPath
.Setup(static p => p.GetFullPath(It.IsAny<string>(), It.IsAny<string>()))
.Returns("/full/keys");
_mockFileSystem.Setup(static fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(static fs => fs.Directory.Exists(It.IsAny<string>()))
.Returns(false);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var action = sut.GetPrimaryKey;
action.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void FileKeyRing_WhenOptionsChangeHandlerInvoked_ItShouldReloadKeys()
{
var initialKeyId = "initial-key";
var initialKeyContent = "initial-key-content";
var updatedKeyId = "updated-key";
var updatedKeyContent = "updated-key-content";
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = initialKeyId
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{initialKeyId}.key"))
.Returns(initialKeyId);
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{updatedKeyId}.key"))
.Returns(updatedKeyId);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.SetupSequence(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns([$"/full/keys/{initialKeyId}.key"]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{initialKeyId}.key"))
.Returns(initialKeyContent);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var initialKey = sut.GetKey(initialKeyId);
initialKey.Should().NotBeNull();
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns([$"/full/keys/{updatedKeyId}.key"]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{updatedKeyId}.key"))
.Returns(updatedKeyContent);
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = updatedKeyId
};
var updatedKey = sut.GetKey(updatedKeyId);
updatedKey.Should().NotBeNull();
updatedKey.KeyId.Should().Be(updatedKeyId);
}
[Fact]
public void FileKeyRing_WhenDirectoryDoesNotExist_ItShouldNotThrowAndLoadKeysReturnsEmpty()
{
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/non-existent-keys",
PrimaryKeyId = "any-key"
};
mockPath
.Setup(p => p.GetFullPath("/non-existent-keys", It.IsAny<string>()))
.Returns("/full/non-existent-keys");
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/non-existent-keys"))
.Returns(false);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var action = () => sut.GetKey("any-key");
action.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void FileKeyRing_WhenKeyFileContainsOnlyWhitespace_ItShouldSkipKeyAndNotLoad()
{
var validKeyId = "valid-key";
var validKeyContent = "valid-key-content";
var whitespaceKeyId = "whitespace-key";
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = validKeyId
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{validKeyId}.key"))
.Returns(validKeyId);
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{whitespaceKeyId}.key"))
.Returns(whitespaceKeyId);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns(
[
$"/full/keys/{validKeyId}.key",
$"/full/keys/{whitespaceKeyId}.key"
]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{validKeyId}.key"))
.Returns(validKeyContent);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{whitespaceKeyId}.key"))
.Returns(" \n\t ");
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var validKey = sut.GetKey(validKeyId);
validKey.Should().NotBeNull();
var action = () => sut.GetKey(whitespaceKeyId);
action.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void FileKeyRing_WhenMultipleKeysExist_ItShouldLoadAllKeys()
{
var key1Id = "key-1";
var key1Content = "key-1-content";
var key2Id = "key-2";
var key2Content = "key-2-content";
var key3Id = "key-3";
var key3Content = "key-3-content";
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = key1Id
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{key1Id}.key"))
.Returns(key1Id);
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{key2Id}.key"))
.Returns(key2Id);
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{key3Id}.key"))
.Returns(key3Id);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns(
[
$"/full/keys/{key1Id}.key",
$"/full/keys/{key2Id}.key",
$"/full/keys/{key3Id}.key"
]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{key1Id}.key"))
.Returns(key1Content);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{key2Id}.key"))
.Returns(key2Content);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{key3Id}.key"))
.Returns(key3Content);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var retrievedKey1 = sut.GetKey(key1Id);
var retrievedKey2 = sut.GetKey(key2Id);
var retrievedKey3 = sut.GetKey(key3Id);
retrievedKey1.Should().NotBeNull();
retrievedKey2.Should().NotBeNull();
retrievedKey3.Should().NotBeNull();
retrievedKey1.KeyId.Should().Be(key1Id);
retrievedKey2.KeyId.Should().Be(key2Id);
retrievedKey3.KeyId.Should().Be(key3Id);
}
[Fact]
public void Dispose_WhenCalled_ItShouldDisposeOptionsChangeHandler()
{
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = "key"
};
mockPath
.Setup(static p => p.GetFullPath(It.IsAny<string>(), It.IsAny<string>()))
.Returns("/full/keys");
_mockFileSystem.Setup(static fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(static fs => fs.Directory.Exists(It.IsAny<string>()))
.Returns(false);
using var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var action = sut.Dispose;
action.Should().NotThrow();
}
[Fact]
public void FileKeyRing_WhenKeyTrimsWhitespace_ItShouldLoadKeyWithTrimmedContent()
{
var keyId = "test-key";
var keyContentWithWhitespace = " \n test-key-content \t ";
var mockPath = new Mock<IPath>();
_mockOptionsMonitor.CurrentValue = new FileKeyRingOptions
{
KeysDirectoryPath = "/keys",
PrimaryKeyId = keyId
};
mockPath
.Setup(p => p.GetFullPath("/keys", It.IsAny<string>()))
.Returns("/full/keys");
mockPath
.Setup(p => p.GetFileNameWithoutExtension($"/full/keys/{keyId}.key"))
.Returns(keyId);
_mockFileSystem.Setup(fs => fs.Path).Returns(mockPath.Object);
_mockFileSystem
.Setup(fs => fs.Directory.Exists("/full/keys"))
.Returns(true);
_mockFileSystem
.Setup(fs => fs.Directory.GetFiles("/full/keys", "*.key"))
.Returns([$"/full/keys/{keyId}.key"]);
_mockFileSystem
.Setup(fs => fs.File.ReadAllText($"/full/keys/{keyId}.key"))
.Returns(keyContentWithWhitespace);
var sut = FileKeyRing.From(_mockOptionsMonitor, _mockFileSystem.Object);
var result = sut.GetKey(keyId);
result.Should().NotBeNull();
result.Key.Should().Be("test-key-content");
}
}
@@ -0,0 +1,380 @@
namespace FiscalOS.Infra.Tests.Unit;
public class JwtOptionsTests
{
[Fact]
public void JwtOptions_WhenCreatedWithDefaults_ItShouldHaveCorrectDefaultValues()
{
var options = new JwtOptions();
options.Issuer.Should().Be(string.Empty);
options.Audience.Should().Be(string.Empty);
options.Secret.Should().Be(string.Empty);
options.ExpiryInMinutes.Should().Be(5);
}
[Fact]
public void JwtOptions_WhenInitializedWithValues_ItShouldHaveCorrectValues()
{
var issuer = "test-issuer";
var audience = "test-audience";
var secret = "test-secret-key";
var expiryInMinutes = 30;
var options = new JwtOptions
{
Issuer = issuer,
Audience = audience,
Secret = secret,
ExpiryInMinutes = expiryInMinutes
};
options.Issuer.Should().Be(issuer);
options.Audience.Should().Be(audience);
options.Secret.Should().Be(secret);
options.ExpiryInMinutes.Should().Be(expiryInMinutes);
}
[Fact]
public void JwtOptions_WhenSecretIsSet_ItShouldReturnSymmetricSecurityKey()
{
var secret = "my-super-secret-key-12345";
var options = new JwtOptions { Secret = secret };
var result = options.Key;
result.Should().NotBeNull();
result.Should().BeOfType<SymmetricSecurityKey>();
}
[Fact]
public void JwtOptions_WhenSecretIsSet_ItShouldEncodeSecretAsUtf8Bytes()
{
var secret = "test-secret";
var options = new JwtOptions { Secret = secret };
var result = options.Key;
var expectedKey = Encoding.UTF8.GetBytes(secret);
result.Key.Should().BeEquivalentTo(expectedKey);
}
[Fact]
public void JwtOptions_WhenSecretChanges_ItShouldReturnUpdatedSymmetricSecurityKey()
{
var initialSecret = "initial-secret";
var newSecret = "new-secret";
var options = new JwtOptions { Secret = initialSecret };
var initialKey = options.Key;
var updatedOptions = options with { Secret = newSecret };
var updatedKey = updatedOptions.Key;
initialKey.Key.Should().NotBeEquivalentTo(updatedKey.Key);
}
[Fact]
public void JwtOptions_WhenUsedAsRecord_ItShouldSupportEquality()
{
var options1 = new JwtOptions
{
Issuer = "issuer",
Audience = "audience",
Secret = "secret",
ExpiryInMinutes = 30
};
var options2 = new JwtOptions
{
Issuer = "issuer",
Audience = "audience",
Secret = "secret",
ExpiryInMinutes = 30
};
options1.Should().Be(options2);
}
[Fact]
public void JwtOptions_WhenCreatedWithDifferentValues_ItShouldNotBeEqual()
{
var options1 = new JwtOptions { Secret = "secret1" };
var options2 = new JwtOptions { Secret = "secret2" };
options1.Should().NotBe(options2);
}
[Fact]
public void JwtOptionsSetup_WhenConstructed_ItShouldStoreConfiguration()
{
var configuration = new ConfigurationBuilder().Build();
var setup = new JwtOptionsSetup(configuration);
setup.Should().NotBeNull();
}
[Fact]
public void Configure_WhenCalledWithValidConfiguration_ItShouldBindJwtOptionsCorrectly()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:Issuer", "test-issuer" },
{ "JwtOptions:Audience", "test-audience" },
{ "JwtOptions:Secret", "test-secret" },
{ "JwtOptions:ExpiryInMinutes", "60" }
});
var configuration = configBuilder.Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.Issuer.Should().Be("test-issuer");
options.Audience.Should().Be("test-audience");
options.Secret.Should().Be("test-secret");
options.ExpiryInMinutes.Should().Be(60);
}
[Fact]
public void Configure_WhenCalledWithPartialConfiguration_ItShouldBindAvailableValuesAndKeepDefaults()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:Issuer", "test-issuer" }
});
var configuration = configBuilder.Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.Issuer.Should().Be("test-issuer");
options.Audience.Should().Be(string.Empty);
options.Secret.Should().Be(string.Empty);
options.ExpiryInMinutes.Should().Be(5);
}
[Fact]
public void Configure_WhenCalledWithMissingSection_ItShouldLeaveOptionsUnchanged()
{
var configuration = new ConfigurationBuilder().Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.Issuer.Should().Be(string.Empty);
options.Audience.Should().Be(string.Empty);
options.Secret.Should().Be(string.Empty);
options.ExpiryInMinutes.Should().Be(5);
}
[Fact]
public void Configure_WhenCalledWithEmptyStringValues_ItShouldBindEmptyStrings()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:Issuer", "" },
{ "JwtOptions:Audience", "" },
{ "JwtOptions:Secret", "" }
});
var configuration = configBuilder.Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.Issuer.Should().Be(string.Empty);
options.Audience.Should().Be(string.Empty);
options.Secret.Should().Be(string.Empty);
}
[Fact]
public void Configure_WhenCalledWithNumericString_ItShouldBindExpiryInMinutesAsInteger()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:ExpiryInMinutes", "120" }
});
var configuration = configBuilder.Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.ExpiryInMinutes.Should().Be(120);
}
[Fact]
public void Configure_WhenCalledMultipleTimes_ItShouldUpdateOptionsEachTime()
{
var configBuilder = new ConfigurationBuilder();
configBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:Issuer", "issuer-1" }
});
var configuration = configBuilder.Build();
var setup = new JwtOptionsSetup(configuration);
var options = new JwtOptions();
setup.Configure(options);
options.Issuer.Should().Be("issuer-1");
var configBuilder2 = new ConfigurationBuilder();
configBuilder2.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "JwtOptions:Issuer", "issuer-2" }
});
var configuration2 = configBuilder2.Build();
var setup2 = new JwtOptionsSetup(configuration2);
setup2.Configure(options);
options.Issuer.Should().Be("issuer-2");
}
[Fact]
public void Configure_WhenCalledWithDefaultScheme_ItShouldSetupJwtBearerOptionsWithValidation()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure(JwtBearerDefaults.AuthenticationScheme, bearerOptions);
bearerOptions.TokenValidationParameters.Should().NotBeNull();
bearerOptions.TokenValidationParameters!.ValidIssuer.Should().Be("test-issuer");
bearerOptions.TokenValidationParameters.ValidAudience.Should().Be("test-audience");
bearerOptions.TokenValidationParameters.ValidateIssuer.Should().BeTrue();
bearerOptions.TokenValidationParameters.ValidateAudience.Should().BeTrue();
bearerOptions.TokenValidationParameters.ValidateIssuerSigningKey.Should().BeTrue();
bearerOptions.TokenValidationParameters.ValidateLifetime.Should().BeTrue();
}
[Fact]
public void Configure_WhenCalledWithAllowExpiredTokensScheme_ItShouldDisableLifetimeValidation()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure(Schemes.AllowExpiredTokens, bearerOptions);
bearerOptions.TokenValidationParameters.Should().NotBeNull();
bearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeFalse();
}
[Fact]
public void Configure_WhenCalledWithNullSchemeName_ItShouldSetupWithValidation()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure(null, bearerOptions);
bearerOptions.TokenValidationParameters.Should().NotBeNull();
bearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeTrue();
}
[Fact]
public void Configure_WhenCalledWithoutNameParameter_ItShouldSetupWithValidation()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure(bearerOptions);
bearerOptions.TokenValidationParameters.Should().NotBeNull();
bearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeTrue();
}
[Fact]
public void Configure_WhenCalledWithDifferentSchemeNames_ItShouldOnlyDisableLifetimeForAllowExpiredTokens()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var defaultBearerOptions = new JwtBearerOptions();
setup.Configure(Schemes.Default, defaultBearerOptions);
var allowExpiredBearerOptions = new JwtBearerOptions();
setup.Configure(Schemes.AllowExpiredTokens, allowExpiredBearerOptions);
defaultBearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeTrue();
allowExpiredBearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeFalse();
}
[Fact]
public void Configure_WhenCalledWithCustomSchemeName_ItShouldUseDefaultValidationBehavior()
{
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = "test-secret-key-for-jwt-validation"
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure("CustomScheme", bearerOptions);
bearerOptions.TokenValidationParameters!.ValidateLifetime.Should().BeTrue();
}
[Fact]
public void Configure_WhenCalled_ItShouldSetIssuerSigningKeyFromJwtOptions()
{
var secret = "test-secret-key-for-jwt-validation";
var jwtOptions = new JwtOptions
{
Issuer = "test-issuer",
Audience = "test-audience",
Secret = secret
};
var options = Options.Create(jwtOptions);
var setup = new JwtBearerOptionsSetup(options);
var bearerOptions = new JwtBearerOptions();
setup.Configure(bearerOptions);
var expectedKey = jwtOptions.Key;
bearerOptions.TokenValidationParameters!.IssuerSigningKey.Should().BeEquivalentTo(expectedKey);
}
}
@@ -1,5 +1,3 @@
using FiscalOS.Infra.Authentication;
namespace FiscalOS.Infra.Tests.Unit;
public class PasswordHasherTests
@@ -0,0 +1,325 @@
namespace FiscalOS.Infra.Tests.Unit;
public class ProblemDetailsAuthResultHandlerTests
{
private readonly ProblemDetailsAuthResultHandler _sut = new();
[Fact]
public async Task HandleAsync_WhenAuthorizationSucceeds_ItShouldCallNextDelegate()
{
var nextCalled = false;
RequestDelegate next = _ =>
{
nextCalled = true;
return Task.CompletedTask;
};
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Success();
await _sut.HandleAsync(next, context, policy, authorizeResult);
nextCalled.Should().BeTrue();
}
[Fact]
public async Task HandleAsync_WhenAuthorizationSucceeds_ItShouldNotModifyResponseStatusCode()
{
var originalStatusCode = StatusCodes.Status200OK;
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext { Response = { StatusCode = originalStatusCode } };
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Success();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.StatusCode.Should().Be(originalStatusCode);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationSucceeds_ItShouldNotWriteToResponseBody()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Success();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Length.Should().Be(0);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldSetStatusCodeTo403()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.StatusCode.Should().Be(StatusCodes.Status403Forbidden);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldSetContentTypeToApplicationProblemJson()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.ContentType.Should().Be("application/problem+json");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldWriteProblemDetailsWithForbiddenTitle()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Title.Should().Be("Forbidden");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldWriteProblemDetailsWithCorrectStatus()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Status.Should().Be(StatusCodes.Status403Forbidden);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldWriteProblemDetailsWithCorrectDetail()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Detail.Should().Be("You do not have permission to access this resource.");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsForbidden_ItShouldNotCallNextDelegate()
{
var nextCalled = false;
RequestDelegate next = _ =>
{
nextCalled = true;
return Task.CompletedTask;
};
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Forbid();
await _sut.HandleAsync(next, context, policy, authorizeResult);
nextCalled.Should().BeFalse();
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldSetStatusCodeTo401()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.StatusCode.Should().Be(StatusCodes.Status401Unauthorized);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldSetContentTypeToApplicationProblemJson()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
var policy = CreateValidAuthorizationPolicy();
var unauthorizedResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, unauthorizedResult);
context.Response.ContentType.Should().Be("application/problem+json");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldWriteProblemDetailsWithUnauthorizedTitle()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var unauthorizedResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, unauthorizedResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Title.Should().Be("Unauthorized");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldWriteProblemDetailsWithCorrectStatus()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var unauthorizedResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, unauthorizedResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Status.Should().Be(StatusCodes.Status401Unauthorized);
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldWriteProblemDetailsWithCorrectDetail()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var unauthorizedResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, unauthorizedResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var problemDetails = JsonSerializer.Deserialize<ProblemDetails>(responseBody);
problemDetails!.Detail.Should().Be("Authentication is required to access this resource.");
}
[Fact]
public async Task HandleAsync_WhenAuthorizationResultIsUnauthorized_ItShouldNotCallNextDelegate()
{
var nextCalled = false;
RequestDelegate next = _ =>
{
nextCalled = true;
return Task.CompletedTask;
};
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var unauthorizedResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, unauthorizedResult);
nextCalled.Should().BeFalse();
}
[Fact]
public async Task HandleAsync_WhenAuthorizationFails_ItShouldWriteValidJsonToProblemDetails()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Seek(0, SeekOrigin.Begin);
using var stream = new StreamReader(context.Response.Body);
var responseBody = await stream.ReadToEndAsync(TestContext.Current.CancellationToken);
var action = () => JsonSerializer.Deserialize<ProblemDetails>(responseBody);
action.Should().NotThrow();
}
[Fact]
public async Task HandleAsync_WhenAuthorizationFails_ItShouldWriteResponseBodyWithContent()
{
RequestDelegate next = _ => Task.CompletedTask;
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
var policy = CreateValidAuthorizationPolicy();
var authorizeResult = PolicyAuthorizationResult.Challenge();
await _sut.HandleAsync(next, context, policy, authorizeResult);
context.Response.Body.Length.Should().BeGreaterThan(0);
}
private static AuthorizationPolicy CreateValidAuthorizationPolicy()
{
var requirement = new DummyAuthorizationRequirement();
return new AuthorizationPolicy([requirement], []);
}
private sealed class DummyAuthorizationRequirement : IAuthorizationRequirement
{
}
}
@@ -0,0 +1,16 @@
namespace FiscalOS.Infra.Tests.Unit;
public class SchemesTests
{
[Fact]
public void Default_WhenCalled_ItShouldHaveExpectedValue()
{
Schemes.Default.Should().Be(JwtBearerDefaults.AuthenticationScheme);
}
[Fact]
public void AllowExpiredTokens_WhenCalled_ItShouldHaveExpectedValue()
{
Schemes.AllowExpiredTokens.Should().Be("AllowExpiredTokens");
}
}
@@ -17,7 +17,8 @@ public class TokenGeneratorTests
[Fact]
public void GenerateAccessToken_WhenCalled_ItShouldReturnValidJwtToken()
{
var user = User.From("testuser", "hashedpassword");
var encryptedDataKey = EncryptedDataKey.From("keyId", "encryptedKey");
var user = User.From("testuser", "hashedpassword", encryptedDataKey);
var madeUp256BitSecret = RandomNumberGenerator.GetBytes(32);
@@ -58,7 +59,8 @@ public class TokenGeneratorTests
[Fact]
public void GenerateRefreshToken_WhenCalled_ItShouldReturnRefreshTokenWithCorrectProperties()
{
var user = User.From("testuser", "hashedpassword");
var encryptedDataKey = EncryptedDataKey.From("keyId", "encryptedKey");
var user = User.From("testuser", "hashedpassword", encryptedDataKey);
var now = DateTimeOffset.UtcNow;
+15 -1
View File
@@ -1,11 +1,25 @@
global using System.Globalization;
global using System.IdentityModel.Tokens.Jwt;
global using System.IO.Abstractions;
global using System.Security.Cryptography;
global using System.Text;
global using System.Text.Json;
global using AwesomeAssertions.Primitives;
global using FiscalOS.Core.Identity;
global using FiscalOS.Core.Security;
global using FiscalOS.Infra.Authentication;
global using FiscalOS.Infra.Authorization;
global using FiscalOS.Infra.Security;
global using FiscalOS.Infra.Tests.Assertions;
global using FiscalOS.Infra.Tests.Mocks;
global using Microsoft.Extensions.Options;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authorization;
global using Microsoft.AspNetCore.Authorization.Policy;
global using Microsoft.AspNetCore.Http;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.Options;
global using Microsoft.IdentityModel.Tokens;