Increase Elsa Secrets test coverage (#7530)
This commit is contained in:
parent
cfa323331d
commit
27e6b81a6e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue