elsa-core/test/unit/Elsa.Secrets.UnitTests/SecretManagerTests.cs
Sipke Schoorstra e2e00ff235
Add secrets module (#7468)
* Add secrets module

* Address Greptile feedback for secrets module

* Handle unavailable secrets in provider adapter

* Address path combine review comments

* Address additional Greptile secrets review

* Address final Greptile secrets feedback

* Handle secrets test payload failures

* address greptile feedback on secrets rotation

* fix secret recreation concurrency

* address greptile secrets followups

* address greptile secrets reliability feedback

* align secret store capabilities
2026-05-20 11:48:01 +02:00

138 lines
5.3 KiB
C#

using Elsa.Secrets.Models;
using Xunit;
namespace Elsa.Secrets.UnitTests;
public class SecretManagerTests
{
private readonly SecretTestFixture _fixture = new();
[Fact]
public async Task CreateAsync_NormalizesTechnicalName_AndDoesNotExposeValueInModel()
{
var secret = await _fixture.Manager.CreateAsync(new CreateSecretRequest
{
Name = "Smtp:Password",
DisplayName = "SMTP password",
Value = "p@ssword"
});
var model = Elsa.Secrets.Services.SecretModelMapper.ToModel(secret);
Assert.Equal("smtp:password", secret.Name);
Assert.Equal("SMTP password", model.DisplayName);
Assert.Equal(1, model.CurrentVersion);
Assert.DoesNotContain(model.GetType().GetProperties(), x => x.Name.Contains("Value", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public async Task RotateAsync_RetiresPreviousVersion_AndKeepsOneActiveVersion()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
var rotated = await _fixture.Manager.RotateAsync("smtp:password", new RotateSecretRequest { Value = "two" });
Assert.Equal(2, rotated.Versions.Count);
Assert.Single(rotated.Versions, x => x.Status == SecretStatus.Active);
Assert.Single(rotated.Versions, x => x.Status == SecretStatus.Retired);
}
[Fact]
public async Task RevokeAsync_PreventsResolution()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await _fixture.Manager.RevokeAsync("smtp:password");
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Resolver.ResolveAsync("smtp:password"));
}
[Fact]
public async Task RotateAsync_RejectsRevokedSecret()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await _fixture.Manager.RevokeAsync("smtp:password");
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Manager.RotateAsync("smtp:password", new RotateSecretRequest { Value = "two" }));
var secret = await _fixture.Manager.GetAsync("smtp:password");
Assert.Equal(SecretStatus.Revoked, secret!.Status);
Assert.All(secret.Versions, x => Assert.Equal(SecretStatus.Revoked, x.Status));
}
[Fact]
public async Task CreateAsync_RejectsDuplicateTechnicalName()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await Assert.ThrowsAsync<InvalidOperationException>(() => _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = " SMTP:PASSWORD ", Value = "two" }));
}
[Fact]
public async Task CreateAsync_AllowsReusingDeletedSecretName()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await _fixture.Manager.DeleteAsync("smtp:password");
var secret = await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = " SMTP:PASSWORD ", Value = "two" });
Assert.Equal("smtp:password", secret.Name);
Assert.Equal(SecretStatus.Active, secret.Status);
Assert.Single(secret.Versions);
Assert.Equal("two", await _fixture.Resolver.ResolveAsync("smtp:password"));
}
[Fact]
public async Task CreateAsync_AllowsOnlyOneConcurrentReuseOfDeletedSecretName()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await _fixture.Manager.DeleteAsync("smtp:password");
var createTasks = Enumerable.Range(0, 2)
.Select(x => TryCreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = x.ToString() }))
.ToArray();
var results = await Task.WhenAll(createTasks);
var stored = await _fixture.Manager.GetAsync("smtp:password");
Assert.Single(results, true);
Assert.NotNull(stored);
Assert.Equal(SecretStatus.Active, stored.Status);
}
[Fact]
public async Task DeleteAsync_RemovesEncryptedPayloadMaterial()
{
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = "smtp:password", Value = "one" });
await _fixture.Manager.DeleteAsync("smtp:password");
var stored = await _fixture.Repository.GetAsync("smtp:password");
Assert.NotNull(stored);
Assert.Equal(SecretStatus.Deleted, stored.Status);
Assert.All(stored.Versions, x => Assert.False(x.Payload.Metadata.ContainsKey("protectedValue")));
}
[Fact]
public async Task CountAsync_ReturnsTotalMatchingItems_NotPageSize()
{
for (var i = 0; i < 3; i++)
await _fixture.Manager.CreateAsync(new CreateSecretRequest { Name = $"smtp:password:{i}", Value = "one" });
var items = await _fixture.Manager.ListAsync(new ListSecretsRequest { PageSize = 1 });
var count = await _fixture.Manager.CountAsync(new ListSecretsRequest { PageSize = 1 });
Assert.Single(items);
Assert.Equal(3, count);
}
private async Task<bool> TryCreateAsync(CreateSecretRequest request)
{
try
{
await _fixture.Manager.CreateAsync(request);
return true;
}
catch (InvalidOperationException)
{
return false;
}
}
}