using Elsa.Persistence.EFCore; using Elsa.Persistence.EFCore.Extensions; using Elsa.Secrets.Contracts; using Elsa.Secrets.Models; using Elsa.Secrets.Persistence.EFCore; using Elsa.Secrets.Persistence.EFCore.Repositories; using Elsa.Secrets.Persistence.EFCore.Sqlite.Extensions; using Elsa.Secrets.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Elsa.Secrets.UnitTests; public class EFCoreSecretRepositoryTests : IAsyncLifetime { private readonly string _databasePath = Path.Join(Path.GetTempPath(), $"elsa-secrets-{Guid.NewGuid():N}.db"); private readonly ServiceProvider _serviceProvider; public EFCoreSecretRepositoryTests() { var services = new ServiceCollection(); var connectionString = $"Data Source={_databasePath}"; services.AddSqliteEntityModelCreatingHandlers(); services.AddDbContextFactory(builder => builder.UseElsaSqlite(typeof(SqliteSecretsPersistenceFeatureExtensions).Assembly, connectionString)); services.AddSingleton(); services.AddScoped>(); services.AddScoped(); _serviceProvider = services.BuildServiceProvider(); } public async Task InitializeAsync() { await using var scope = _serviceProvider.CreateAsyncScope(); var factory = scope.ServiceProvider.GetRequiredService>(); await using var dbContext = await factory.CreateDbContextAsync(); await dbContext.Database.MigrateAsync(); } public async Task DisposeAsync() { await _serviceProvider.DisposeAsync(); if (File.Exists(_databasePath)) File.Delete(_databasePath); } [Fact] public async Task PersistsSecretAggregate() { await using var scope = _serviceProvider.CreateAsyncScope(); var repository = scope.ServiceProvider.GetRequiredService(); var secret = new Secret { Name = "smtp:password", DisplayName = "SMTP password", Tags = ["API-Key"], Versions = { new SecretVersion { Version = 1, Payload = new SecretPayload { Metadata = { ["ProtectedValue"] = "ciphertext" } } } } }; await repository.AddAsync(secret); var reloaded = await repository.GetAsync("smtp:password"); Assert.NotNull(reloaded); Assert.Equal("SMTP password", reloaded.DisplayName); Assert.Contains("api-key", reloaded.Tags); Assert.True(reloaded.Versions.Single().Payload.Metadata.ContainsKey("protectedvalue")); Assert.Equal(1, reloaded.Versions.Single().Version); } [Fact] public async Task TryAddOrReplaceDeletedAsync_ReplacesOnlyDeletedSecret() { await using var scope = _serviceProvider.CreateAsyncScope(); var repository = scope.ServiceProvider.GetRequiredService(); 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 NameLookups_AreCaseInsensitive() { await using var scope = _serviceProvider.CreateAsyncScope(); var repository = scope.ServiceProvider.GetRequiredService(); await repository.AddAsync(new Secret { Name = "SMTP:PASSWORD", DisplayName = "SMTP password" }); var reloaded = await repository.GetAsync("smtp:password"); var whitespaceReloaded = await repository.GetAsync(" SMTP:PASSWORD "); var activeReplacementResult = await repository.TryAddOrReplaceDeletedAsync(new Secret { Name = "smtp:password", DisplayName = "Replacement password" }); var duplicateException = await Assert.ThrowsAsync(() => repository.AddAsync(new Secret { Name = "smtp:password", DisplayName = "Duplicate password" })); Assert.NotNull(reloaded); Assert.NotNull(whitespaceReloaded); Assert.Equal("SMTP:PASSWORD", reloaded.Name); Assert.Equal(reloaded.Id, whitespaceReloaded.Id); Assert.False(activeReplacementResult); Assert.Equal("A secret named 'smtp:password' already exists.", duplicateException.Message); } [Fact] public async Task TryAddOrReplaceDeletedAsync_WhenReplacingDeletedSecret_PersistsReplacementId() { await using var scope = _serviceProvider.CreateAsyncScope(); var repository = scope.ServiceProvider.GetRequiredService(); await repository.SaveAsync(new Secret { Id = "old", Name = "smtp:password", DisplayName = "Deleted password", Status = SecretStatus.Deleted }); var replacement = new Secret { Id = "new", Name = "SMTP:PASSWORD", DisplayName = "Replacement password" }; var result = await repository.TryAddOrReplaceDeletedAsync(replacement); var reloaded = await repository.GetAsync("smtp:password"); Assert.True(result); Assert.NotNull(reloaded); Assert.Equal("new", reloaded.Id); Assert.Equal("Replacement password", reloaded.DisplayName); Assert.Equal(SecretStatus.Active, reloaded.Status); } }