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(() => _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(() => _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(() => _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 TryCreateAsync(CreateSecretRequest request) { try { await _fixture.Manager.CreateAsync(request); return true; } catch (InvalidOperationException) { return false; } } }