too much for one commit
This commit is contained in:
518
tests/EntKube.Secrets.Tests/Domain/VaultTests.cs
Normal file
518
tests/EntKube.Secrets.Tests/Domain/VaultTests.cs
Normal file
@@ -0,0 +1,518 @@
|
||||
using EntKube.Secrets.Crypto;
|
||||
using EntKube.Secrets.Domain;
|
||||
using EntKube.Secrets.Infrastructure;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Secrets.Tests.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the multi-tenant Vault aggregate. Each tenant gets their own
|
||||
/// master encryption key (MEK) for cryptographic isolation.
|
||||
///
|
||||
/// The vault manages per-tenant MEKs — initialization, auto-unseal, and
|
||||
/// secret operations are all scoped to a specific tenant.
|
||||
/// </summary>
|
||||
public class VaultTests
|
||||
{
|
||||
private readonly InMemoryVaultRepository repository = new();
|
||||
private readonly byte[] kek = AesGcmEncryptor.GenerateKey();
|
||||
private readonly Guid tenantId = Guid.NewGuid();
|
||||
|
||||
private Vault CreateVault() => new(repository);
|
||||
|
||||
// ─── Initialization ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_GeneratesMasterKeyAndAutoUnseals()
|
||||
{
|
||||
// Arrange — A fresh vault backed by an empty repository.
|
||||
|
||||
Vault vault = CreateVault();
|
||||
|
||||
// Act — Initialize a tenant with KEK, 5 Shamir shares, threshold of 3.
|
||||
|
||||
InitializationResult result = await vault.InitializeAsync(tenantId, kek, totalShares: 5, threshold: 3);
|
||||
|
||||
// Assert — Shares returned for DR, tenant's vault is immediately unsealed.
|
||||
|
||||
result.Shares.Should().HaveCount(5);
|
||||
result.Threshold.Should().Be(3);
|
||||
vault.IsTenantInitialized(tenantId).Should().BeTrue();
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Initialize_WhenAlreadyInitialized_Fails()
|
||||
{
|
||||
// A tenant's vault can only be initialized once.
|
||||
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
|
||||
Func<Task> act = () => vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*already initialized*");
|
||||
}
|
||||
|
||||
// ─── Auto-Unseal (Normal K8s Startup) ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AutoUnseal_WithCorrectKek_UnsealsAllTenants()
|
||||
{
|
||||
// Arrange — Initialize two tenants, then seal both.
|
||||
|
||||
Guid tenant1 = Guid.NewGuid();
|
||||
Guid tenant2 = Guid.NewGuid();
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenant1, kek, totalShares: 1, threshold: 1);
|
||||
await vault.InitializeAsync(tenant2, kek, totalShares: 1, threshold: 1);
|
||||
await vault.SealAsync();
|
||||
|
||||
// Act — A new vault instance auto-unseals all tenants.
|
||||
|
||||
Vault vault2 = CreateVault();
|
||||
await vault2.AutoUnsealAsync(kek);
|
||||
|
||||
// Assert — Both tenants unsealed.
|
||||
|
||||
vault2.IsTenantSealed(tenant1).Should().BeFalse();
|
||||
vault2.IsTenantSealed(tenant2).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoUnseal_WithWrongKek_Fails()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.SealAsync();
|
||||
|
||||
byte[] wrongKek = AesGcmEncryptor.GenerateKey();
|
||||
Vault vault2 = CreateVault();
|
||||
|
||||
Func<Task> act = () => vault2.AutoUnsealAsync(wrongKek);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*KEK*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoUnseal_WhenNotInitialized_Fails()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
|
||||
Func<Task> act = () => vault.AutoUnsealAsync(kek);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not initialized*");
|
||||
}
|
||||
|
||||
// ─── Manual Unseal (Disaster Recovery) ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ManualUnseal_WithEnoughShares_UnsealsVault()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
InitializationResult initResult = await vault.InitializeAsync(tenantId, kek, totalShares: 5, threshold: 3);
|
||||
await vault.SealAsync(tenantId);
|
||||
vault.IsTenantSealed(tenantId).Should().BeTrue();
|
||||
|
||||
// Act — Provide 3 of 5 shares.
|
||||
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[0]);
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[2]);
|
||||
UnsealResult unsealResult = await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[4]);
|
||||
|
||||
unsealResult.IsUnsealed.Should().BeTrue();
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ManualUnseal_WithInsufficientShares_RemainsSealed()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
InitializationResult initResult = await vault.InitializeAsync(tenantId, kek, totalShares: 5, threshold: 3);
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
UnsealResult result1 = await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[0]);
|
||||
UnsealResult result2 = await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[1]);
|
||||
|
||||
result1.IsUnsealed.Should().BeFalse();
|
||||
result1.SharesProvided.Should().Be(1);
|
||||
result1.SharesRequired.Should().Be(3);
|
||||
|
||||
result2.IsUnsealed.Should().BeFalse();
|
||||
result2.SharesProvided.Should().Be(2);
|
||||
|
||||
vault.IsTenantSealed(tenantId).Should().BeTrue();
|
||||
}
|
||||
|
||||
// ─── Multi-Instance (Simulating K8s Pods) ────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task MultiInstance_BothPodsCanAutoUnseal()
|
||||
{
|
||||
Vault pod1 = CreateVault();
|
||||
await pod1.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
Vault pod2 = CreateVault();
|
||||
await pod2.AutoUnsealAsync(kek);
|
||||
|
||||
pod1.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
pod2.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiInstance_SecretWrittenByOnePodReadableByAnother()
|
||||
{
|
||||
Vault pod1 = CreateVault();
|
||||
await pod1.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
Vault pod2 = CreateVault();
|
||||
await pod2.AutoUnsealAsync(kek);
|
||||
|
||||
await pod1.PutSecretAsync(tenantId, "clusters/prod/dns-cred", "cloudflare-api-token");
|
||||
|
||||
string? value = await pod2.GetSecretAsync(tenantId, "clusters/prod/dns-cred");
|
||||
value.Should().Be("cloudflare-api-token");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiInstance_SealingOnePodDoesNotAffectOther()
|
||||
{
|
||||
Vault pod1 = CreateVault();
|
||||
await pod1.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await pod1.PutSecretAsync(tenantId, "shared/secret", "hello");
|
||||
|
||||
Vault pod2 = CreateVault();
|
||||
await pod2.AutoUnsealAsync(kek);
|
||||
|
||||
await pod1.SealAsync(tenantId);
|
||||
|
||||
pod1.IsTenantSealed(tenantId).Should().BeTrue();
|
||||
pod2.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
|
||||
string? value = await pod2.GetSecretAsync(tenantId, "shared/secret");
|
||||
value.Should().Be("hello");
|
||||
}
|
||||
|
||||
// ─── Multi-Tenant Isolation ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task MultiTenant_SecretsAreCryptographicallyIsolated()
|
||||
{
|
||||
// Each tenant has their own MEK. A secret written for tenant A
|
||||
// cannot be decrypted by tenant B's MEK.
|
||||
|
||||
Guid tenantA = Guid.NewGuid();
|
||||
Guid tenantB = Guid.NewGuid();
|
||||
Vault vault = CreateVault();
|
||||
|
||||
await vault.InitializeAsync(tenantA, kek, totalShares: 1, threshold: 1);
|
||||
await vault.InitializeAsync(tenantB, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
// Each tenant writes a secret at the same logical key but different paths.
|
||||
|
||||
await vault.PutSecretAsync(tenantA, "tenants/a/config/api-key", "tenant-a-secret");
|
||||
await vault.PutSecretAsync(tenantB, "tenants/b/config/api-key", "tenant-b-secret");
|
||||
|
||||
// Each tenant can only read their own secrets.
|
||||
|
||||
string? valueA = await vault.GetSecretAsync(tenantA, "tenants/a/config/api-key");
|
||||
string? valueB = await vault.GetSecretAsync(tenantB, "tenants/b/config/api-key");
|
||||
|
||||
valueA.Should().Be("tenant-a-secret");
|
||||
valueB.Should().Be("tenant-b-secret");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiTenant_SealingOneTenantDoesNotAffectAnother()
|
||||
{
|
||||
Guid tenantA = Guid.NewGuid();
|
||||
Guid tenantB = Guid.NewGuid();
|
||||
Vault vault = CreateVault();
|
||||
|
||||
await vault.InitializeAsync(tenantA, kek, totalShares: 1, threshold: 1);
|
||||
await vault.InitializeAsync(tenantB, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
// Seal only tenant A.
|
||||
|
||||
await vault.SealAsync(tenantA);
|
||||
|
||||
vault.IsTenantSealed(tenantA).Should().BeTrue();
|
||||
vault.IsTenantSealed(tenantB).Should().BeFalse();
|
||||
}
|
||||
|
||||
// ─── Seal / Unseal Lifecycle ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Seal_ClearsMasterKeyFromMemory()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
vault.IsTenantSealed(tenantId).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SealThenAutoUnseal_Works()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
|
||||
await vault.SealAsync();
|
||||
vault.IsSealed.Should().BeTrue();
|
||||
|
||||
await vault.AutoUnsealAsync(kek);
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RepeatedSealUnsealCycles_Work()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
InitializationResult initResult = await vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
|
||||
await vault.SealAsync();
|
||||
await vault.AutoUnsealAsync(kek);
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
|
||||
await vault.SealAsync(tenantId);
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[0]);
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[1]);
|
||||
vault.IsTenantSealed(tenantId).Should().BeFalse();
|
||||
}
|
||||
|
||||
// ─── KEK Rotation ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ReWrapMasterKey_AllowsAutoUnsealWithNewKek()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.PutSecretAsync(tenantId, "test/secret", "original-value");
|
||||
|
||||
byte[] newKek = AesGcmEncryptor.GenerateKey();
|
||||
|
||||
await vault.ReWrapMasterKeyAsync(tenantId, newKek);
|
||||
|
||||
await vault.SealAsync();
|
||||
await vault.AutoUnsealAsync(newKek);
|
||||
|
||||
string? value = await vault.GetSecretAsync(tenantId, "test/secret");
|
||||
value.Should().Be("original-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReWrapMasterKey_OldKekNoLongerWorks()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
byte[] newKek = AesGcmEncryptor.GenerateKey();
|
||||
await vault.ReWrapMasterKeyAsync(tenantId, newKek);
|
||||
await vault.SealAsync();
|
||||
|
||||
Vault vault2 = CreateVault();
|
||||
|
||||
Func<Task> act = () => vault2.AutoUnsealAsync(kek);
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
// ─── Secret Operations ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task PutSecret_WhenUnsealed_StoresEncryptedSecret()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "clusters/prod/letsencrypt/dns-credential", "cloudflare-api-token-value");
|
||||
|
||||
string? retrieved = await vault.GetSecretAsync(tenantId, "clusters/prod/letsencrypt/dns-credential");
|
||||
retrieved.Should().Be("cloudflare-api-token-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutSecret_WhenSealed_ThrowsVaultSealedException()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
Func<Task> act = () => vault.PutSecretAsync(tenantId, "path/to/secret", "value");
|
||||
await act.Should().ThrowAsync<VaultSealedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecret_WhenSealed_ThrowsVaultSealedException()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.PutSecretAsync(tenantId, "my/secret", "value");
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
Func<Task> act = () => vault.GetSecretAsync(tenantId, "my/secret");
|
||||
await act.Should().ThrowAsync<VaultSealedException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecret_NonExistentPath_ReturnsNull()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
string? result = await vault.GetSecretAsync(tenantId, "does/not/exist");
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutSecret_OverwritesExistingSecret()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "old-value");
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "new-value");
|
||||
|
||||
string? result = await vault.GetSecretAsync(tenantId, "config/api-key");
|
||||
|
||||
result.Should().Be("new-value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PutSecret_CreatesVersionHistory()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "v1");
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "v2");
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "v3");
|
||||
|
||||
string? current = await vault.GetSecretAsync(tenantId, "config/api-key");
|
||||
int versionCount = await vault.GetSecretVersionCountAsync(tenantId, "config/api-key");
|
||||
|
||||
current.Should().Be("v3");
|
||||
versionCount.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSecretVersion_RetrievesSpecificVersion()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "version-one");
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "version-two");
|
||||
await vault.PutSecretAsync(tenantId, "config/api-key", "version-three");
|
||||
|
||||
string? v1 = await vault.GetSecretVersionAsync(tenantId, "config/api-key", version: 1);
|
||||
string? v2 = await vault.GetSecretVersionAsync(tenantId, "config/api-key", version: 2);
|
||||
string? v3 = await vault.GetSecretVersionAsync(tenantId, "config/api-key", version: 3);
|
||||
|
||||
v1.Should().Be("version-one");
|
||||
v2.Should().Be("version-two");
|
||||
v3.Should().Be("version-three");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSecret_SoftDeletesAndPreventsRetrieval()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.PutSecretAsync(tenantId, "temp/secret", "temporary-value");
|
||||
|
||||
await vault.DeleteSecretAsync(tenantId, "temp/secret");
|
||||
|
||||
string? result = await vault.GetSecretAsync(tenantId, "temp/secret");
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListSecrets_ReturnsPathsWithoutValues()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "clusters/prod/dns-cred", "value1");
|
||||
await vault.PutSecretAsync(tenantId, "clusters/prod/tls-key", "value2");
|
||||
await vault.PutSecretAsync(tenantId, "clusters/staging/dns-cred", "value3");
|
||||
|
||||
List<string> paths = await vault.ListSecretsAsync(tenantId, "clusters/prod/");
|
||||
|
||||
paths.Should().HaveCount(2);
|
||||
paths.Should().Contain("clusters/prod/dns-cred");
|
||||
paths.Should().Contain("clusters/prod/tls-key");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListSecrets_WhenSealed_ThrowsVaultSealedException()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
Func<Task> act = () => vault.ListSecretsAsync(tenantId, "any/");
|
||||
await act.Should().ThrowAsync<VaultSealedException>();
|
||||
}
|
||||
|
||||
// ─── Secrets Persist Across Seal/Unseal ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Secrets_SurviveSealAutoUnsealCycle()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "persistent/secret", "survive-seal-unseal");
|
||||
|
||||
await vault.SealAsync();
|
||||
|
||||
await vault.AutoUnsealAsync(kek);
|
||||
|
||||
string? recovered = await vault.GetSecretAsync(tenantId, "persistent/secret");
|
||||
recovered.Should().Be("survive-seal-unseal");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Secrets_SurviveManualShamirUnseal()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
InitializationResult initResult = await vault.InitializeAsync(tenantId, kek, totalShares: 3, threshold: 2);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "dr/secret", "disaster-recovery-value");
|
||||
|
||||
await vault.SealAsync(tenantId);
|
||||
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[0]);
|
||||
await vault.ProvideUnsealShareAsync(tenantId, initResult.Shares[1]);
|
||||
|
||||
string? recovered = await vault.GetSecretAsync(tenantId, "dr/secret");
|
||||
recovered.Should().Be("disaster-recovery-value");
|
||||
}
|
||||
|
||||
// ─── Audit ───────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Operations_AreAuditLogged()
|
||||
{
|
||||
Vault vault = CreateVault();
|
||||
await vault.InitializeAsync(tenantId, kek, totalShares: 1, threshold: 1);
|
||||
|
||||
await vault.PutSecretAsync(tenantId, "audit/test", "value");
|
||||
await vault.GetSecretAsync(tenantId, "audit/test");
|
||||
await vault.DeleteSecretAsync(tenantId, "audit/test");
|
||||
|
||||
List<AuditEntry> entries = await vault.GetAuditLogAsync();
|
||||
|
||||
entries.Should().Contain(e => e.Operation == "initialize");
|
||||
entries.Should().Contain(e => e.Operation == "put" && e.Path == "audit/test");
|
||||
entries.Should().Contain(e => e.Operation == "get" && e.Path == "audit/test");
|
||||
entries.Should().Contain(e => e.Operation == "delete" && e.Path == "audit/test");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user