Increase Elsa Secrets test coverage (#7530)

This commit is contained in:
Sipke Schoorstra 2026-05-23 15:47:56 +02:00 committed by GitHub
parent cfa323331d
commit 27e6b81a6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 180 additions and 17 deletions

View file

@ -7,6 +7,16 @@ public class SecretManagerTests
{
private readonly SecretTestFixture _fixture = new();
[Theory]
[InlineData("")]
[InlineData("a")]
[InlineData("1secret")]
[InlineData("secret name")]
public async Task CreateAsync_RejectsInvalidTechnicalNames(string name)
{
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = name, Value = "one" }));
}
[Fact]
public async Task CreateAsync_NormalizesTechnicalName_AndDoesNotExposeValueInModel()
{
@ -79,6 +89,47 @@ public class SecretManagerTests
Assert.Equal("two", await _fixture.Resolver.ResolveAsync("smtp:password"));
}
[Fact]
public async Task CreateAsync_AllowsEncryptedCertificateWithThumbprintMetadata()
{
var secret = await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "tls:certificate",
TypeName = SecretTypeNames.X509Certificate,
Value = " ",
Metadata = new Dictionary<string, string> { ["thumbprint"] = "ABC123" }
});
Assert.Equal(SecretTypeNames.X509Certificate, secret.TypeName);
Assert.Equal("ABC123", secret.Versions.Single().Payload.Metadata["thumbprint"]);
}
[Theory]
[InlineData(SecretTypeNames.Text, SecretStoreNames.Encrypted, null, null)]
[InlineData(SecretTypeNames.RsaKey, SecretStoreNames.Encrypted, " ", null)]
[InlineData(SecretTypeNames.RsaKey, SecretStoreNames.Configuration, null, " ")]
[InlineData(SecretTypeNames.X509Certificate, SecretStoreNames.Encrypted, " ", null)]
[InlineData(SecretTypeNames.X509Certificate, SecretStoreNames.Configuration, null, " ")]
public async Task CreateAsync_RejectsInvalidPayloadForTypeAndStore(string typeName, string storeName, string? value, string? configurationKey)
{
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = $"secret:{Guid.NewGuid():N}",
TypeName = typeName,
StoreName = storeName,
Value = value,
ConfigurationKey = configurationKey
}));
}
[Fact]
public async Task RotateAsync_RejectsInvalidReplacementPayload()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Manager.RotateAsync("smtp:password", new RotateSecretRequest()));
}
[Fact]
public async Task CreateAsync_AllowsOnlyOneConcurrentReuseOfDeletedSecretName()
{
@ -122,6 +173,66 @@ public class SecretManagerTests
Assert.Equal(3, count);
}
[Fact]
public async Task ListPageAsync_AppliesFiltersBeforePaging()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "smtp:password",
DisplayName = "SMTP Password",
Description = "Production credential",
Scope = "Production",
Value = "one"
});
await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "api:key",
DisplayName = "API Key",
Description = "Production credential",
Scope = "production",
Value = "two"
});
await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "dev:token",
Description = "Development credential",
Scope = "development",
Value = "three"
});
await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "old:credential",
Description = "Production credential",
Scope = "production",
Value = "four"
});
await _fixture.Manager.RevokeAsync("old:credential");
var page = await _fixture.Manager.ListPageAsync(new ListSecretsRequest
{
Search = "credential",
TypeNames = [SecretTypeNames.Text],
StoreNames = [SecretStoreNames.Encrypted],
Scope = "PRODUCTION",
Status = SecretStatus.Active,
Page = 1,
PageSize = 1
});
Assert.Equal(2, page.TotalCount);
Assert.Single(page.Items);
Assert.Equal("smtp:password", page.Items.Single().Name);
}
[Fact]
public async Task TestAsync_ReturnsFailedResult_WhenSecretDoesNotExist()
{
var result = await _fixture.Manager.TestAsync("missing:secret");
Assert.False(result.Succeeded);
Assert.Contains("missing:secret", result.Error);
}
private async Task<bool> TryCreateAsync(CreateSecretRequest request)
{
try

View file

@ -40,13 +40,28 @@ public class SecretStoreTests
Assert.Equal("configured-secret", value);
}
[Fact]
public async Task ConfigurationStore_TestAsync_ReturnsFalseWhenConfiguredValueIsMissing()
{
var fixture = new SecretTestFixture();
await fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "smtp:password",
StoreName = SecretStoreNames.Configuration,
ConfigurationKey = "MissingPassword"
});
var result = await fixture.Manager.TestAsync("smtp:password");
Assert.False(result.Succeeded);
Assert.Equal("Secret value is unavailable.", result.Error);
}
[Fact]
public async Task FileRepository_PersistsSecretAggregate()
{
var path = Path.Join(Path.GetTempPath(), $"elsa-secrets-{Guid.NewGuid():N}.json");
try
await WithFileRepositoryAsync(async (repository, path) =>
{
var repository = new FileSecretRepository(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { RepositoryFilePath = path }));
var secret = new Secret
{
Name = "smtp:password",
@ -65,22 +80,49 @@ public class SecretStoreTests
Assert.Contains("api-key", reloaded.Tags);
Assert.True(reloaded.Versions.Single().Payload.Metadata.ContainsKey("protectedvalue"));
Assert.Equal(1, reloaded.Versions.Single().Version);
}
finally
});
}
[Fact]
public async Task FileRepository_SaveAsync_AddsAndUpdatesSecret()
{
await WithFileRepositoryAsync(async (repository, _) =>
{
if (File.Exists(path))
File.Delete(path);
}
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "Updated password" });
var reloaded = await repository.GetAsync("smtp:password");
Assert.NotNull(reloaded);
Assert.Equal("Updated password", reloaded.DisplayName);
});
}
[Fact]
public async Task FileRepository_TryAddOrReplaceDeletedAsync_ReplacesOnlyDeletedSecret()
{
await WithFileRepositoryAsync(async (repository, _) =>
{
await repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
var activeReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "Active replacement" });
await repository.SaveAsync(new Secret { Name = "smtp:password", DisplayName = "Deleted password", Status = SecretStatus.Deleted });
var deletedReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "Replacement password" });
var reloaded = await repository.GetAsync("smtp:password");
Assert.False(activeReplacementResult);
Assert.True(deletedReplacementResult);
Assert.NotNull(reloaded);
Assert.Equal("Replacement password", reloaded.DisplayName);
Assert.Equal(SecretStatus.Active, reloaded.Status);
});
}
[Fact]
public async Task FileRepository_RecoversFromCorruptJson()
{
var path = Path.Join(Path.GetTempPath(), $"elsa-secrets-{Guid.NewGuid():N}.json");
try
await WithFileRepositoryAsync(async (repository, path) =>
{
await File.WriteAllTextAsync(path, "{not-valid-json");
var repository = new FileSecretRepository(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { RepositoryFilePath = path }));
var secrets = await repository.ListAsync();
await repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "SMTP password" });
@ -88,12 +130,7 @@ public class SecretStoreTests
Assert.Empty(secrets);
Assert.NotNull(reloaded);
}
finally
{
if (File.Exists(path))
File.Delete(path);
}
});
}
[Fact]
@ -117,4 +154,19 @@ public class SecretStoreTests
Assert.Single(reloaded.Versions);
Assert.True(reloaded.Versions.Single().Payload.Metadata.ContainsKey("protectedValue"));
}
private static async Task WithFileRepositoryAsync(Func<FileSecretRepository, string, Task> test)
{
var path = Path.Join(Path.GetTempPath(), $"elsa-secrets-{Guid.NewGuid():N}.json");
try
{
var repository = new FileSecretRepository(Microsoft.Extensions.Options.Options.Create(new SecretsOptions { RepositoryFilePath = path }));
await test(repository, path);
}
finally
{
if (File.Exists(path))
File.Delete(path);
}
}
}