< Summary

Information
Class: Elsa.Secrets.Repositories.InMemorySecretRepository
Assembly: Elsa.Secrets
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets/Repositories/InMemorySecretRepository.cs
Line coverage
99%
Covered lines: 109
Uncovered lines: 1
Coverable lines: 110
Total lines: 214
Line coverage: 99%
Branch coverage
97%
Covered branches: 35
Total branches: 36
Branch coverage: 97.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
GetAsync(...)100%22100%
ListAsync(...)100%11100%
AddAsync(...)100%44100%
TryAddOrReplaceDeletedAsync(...)100%1818100%
SaveAsync(...)100%88100%
FindVisible(...)100%22100%
EnsureIdAvailable(...)50%2266.66%
Clone(...)100%11100%
Clone(...)100%11100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets/Repositories/InMemorySecretRepository.cs

#LineLine coverage
 1using Elsa.Common.Multitenancy;
 2using Elsa.Secrets.Contracts;
 3using Elsa.Secrets.Services;
 4using Elsa.Tenants.Options;
 5using Microsoft.Extensions.Options;
 6
 7namespace Elsa.Secrets.Repositories;
 8
 9/// <summary>
 10/// An in-memory secret repository that mirrors the EF Core tenant contract.
 11/// </summary>
 12/// <remarks>
 13/// Secrets are keyed by their stable ID so two tenants can own the same case-insensitive name. Name
 14/// uniqueness and updates are resolved under the ambient tenant, while reads are filtered through
 15/// <see cref="TenantVisibility"/>. This is deliberately the same shape as the persisted contract rather
 16/// than a test-only global name dictionary.
 17/// </remarks>
 18public class InMemorySecretRepository : ISecretRepository
 19{
 20    private readonly ITenantAccessor? tenantAccessor;
 21    private readonly bool tenancyEnabled;
 22    private readonly ISecretNameValidator nameValidator;
 23
 24    public InMemorySecretRepository(ITenantAccessor? tenantAccessor = null)
 6725        : this(tenantAccessor, tenantAccessor is not null, new DefaultSecretNameValidator())
 26    {
 6727    }
 28
 29    // Keep the pre-tenancy parameterless constructor in the public binary surface. Optional parameters do
 30    // not emit a zero-argument constructor for existing binaries to bind to.
 5431    public InMemorySecretRepository() : this(null)
 32    {
 5433    }
 34
 35    public InMemorySecretRepository(
 36        IOptions<TenantsOptions> tenantsOptions,
 37        ISecretNameValidator nameValidator,
 38        ITenantAccessor? tenantAccessor = null)
 339        : this(tenantAccessor, tenantsOptions.Value.IsEnabled, nameValidator)
 40    {
 341    }
 42
 7043    private InMemorySecretRepository(ITenantAccessor? tenantAccessor, bool tenancyEnabled, ISecretNameValidator nameVali
 44    {
 7045        this.tenantAccessor = tenantAccessor;
 7046        this.tenancyEnabled = tenancyEnabled;
 7047        this.nameValidator = nameValidator;
 7048    }
 49
 7050    private readonly Dictionary<string, Secret> _secrets = new(StringComparer.Ordinal);
 7051    private readonly object _sync = new();
 52
 53    public Task<Secret?> GetAsync(string normalizedName, CancellationToken cancellationToken = default)
 54    {
 9455        lock (_sync)
 56        {
 9457            var secret = FindVisible(normalizedName);
 9458            return Task.FromResult(secret is null ? null : Clone(secret));
 59        }
 9460    }
 61
 62    public Task<IReadOnlyCollection<Secret>> ListAsync(CancellationToken cancellationToken = default)
 63    {
 1164        lock (_sync)
 65        {
 1166            var secrets = _secrets.Values
 2767                .Where(x => SecretRepositoryTenant.IsVisible(x, tenantAccessor, tenancyEnabled))
 1168                .Select(Clone)
 1169                .ToList();
 1170            return Task.FromResult<IReadOnlyCollection<Secret>>(secrets);
 71        }
 1172    }
 73
 74    public Task AddAsync(Secret secret, CancellationToken cancellationToken = default)
 75    {
 4176        SecretRepositoryTenant.Stamp(secret, tenantAccessor, tenancyEnabled);
 77
 4178        lock (_sync)
 79        {
 4180            if (FindVisible(secret.Name) is not null)
 681                throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
 82
 4883            if (_secrets.Values.Any(x => SecretRepositoryTenant.HasSameTenantName(x, secret, nameValidator, tenancyEnabl
 284                throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
 85
 3386            EnsureIdAvailable(secret);
 3387            _secrets.Add(secret.Id, Clone(secret));
 3388        }
 89
 3390        return Task.CompletedTask;
 91    }
 92
 93    public Task<bool> TryAddOrReplaceDeletedAsync(Secret secret, CancellationToken cancellationToken = default)
 94    {
 4695        SecretRepositoryTenant.Stamp(secret, tenantAccessor, tenancyEnabled);
 96
 4697        lock (_sync)
 98        {
 4699            var existing = FindVisible(secret.Name);
 46100            if (existing is not null)
 101            {
 11102                if (existing.IsLifecycleManaged || existing.Status != SecretStatus.Deleted || !SecretRepositoryTenant.Ca
 103                {
 5104                    return Task.FromResult(false);
 105                }
 106
 6107                var replacement = Clone(secret);
 6108                if (tenancyEnabled)
 2109                    replacement.TenantId = existing.TenantId;
 110
 111                // Validate before removing the deleted row. Reusing its own ID is valid; another row's ID
 112                // is a collision and must leave the deleted row untouched when validation fails.
 6113                if (replacement.Id != existing.Id && _secrets.ContainsKey(replacement.Id))
 1114                    return Task.FromResult(false);
 115
 5116                _secrets.Remove(existing.Id);
 5117                _secrets.Add(replacement.Id, replacement);
 5118                return Task.FromResult(true);
 119            }
 120
 50121            if (_secrets.Values.Any(x => SecretRepositoryTenant.HasSameTenantName(x, secret, nameValidator, tenancyEnabl
 2122                return Task.FromResult(false);
 123
 124            // Insert-side ID collisions must have the same non-mutating Try contract as the file repository.
 33125            if (_secrets.ContainsKey(secret.Id))
 1126                return Task.FromResult(false);
 127
 32128            _secrets.Add(secret.Id, Clone(secret));
 32129            return Task.FromResult(true);
 130        }
 46131    }
 132
 133    public Task SaveAsync(Secret secret, CancellationToken cancellationToken = default)
 134    {
 23135        SecretRepositoryTenant.Stamp(secret, tenantAccessor, tenancyEnabled);
 136
 23137        lock (_sync)
 138        {
 23139            var existing = FindVisible(secret.Name);
 23140            if (existing is not null)
 141            {
 18142                if (!SecretRepositoryTenant.CanReplace(existing, secret, tenantAccessor, tenancyEnabled))
 1143                    throw new InvalidOperationException($"A secret named '{secret.Name}' belongs to another tenant.");
 144
 145                // EF updates the row found by name, retaining its primary key and tenant ownership.
 17146                var replacement = Clone(secret);
 17147                replacement.Id = existing.Id;
 17148                replacement.ManagedOwnerId = existing.ManagedOwnerId;
 17149                replacement.ManagedGenerationId = existing.ManagedGenerationId;
 150
 17151                if (tenancyEnabled)
 2152                    replacement.TenantId = existing.TenantId;
 153
 17154                _secrets[existing.Id] = replacement;
 17155                return Task.CompletedTask;
 156            }
 157
 8158            if (_secrets.Values.Any(x => SecretRepositoryTenant.HasSameTenantName(x, secret, nameValidator, tenancyEnabl
 2159                throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
 160
 3161            EnsureIdAvailable(secret);
 3162            _secrets.Add(secret.Id, Clone(secret));
 3163        }
 164
 3165        return Task.CompletedTask;
 17166    }
 167
 168    private Secret? FindVisible(string name) =>
 374169        _secrets.Values.FirstOrDefault(x => SecretRepositoryTenant.IsVisible(x, tenantAccessor, tenancyEnabled) && Secre
 170
 171    private void EnsureIdAvailable(Secret secret)
 172    {
 36173        if (_secrets.ContainsKey(secret.Id))
 0174            throw new InvalidOperationException($"A secret with ID '{secret.Id}' already exists.");
 36175    }
 176
 177    private static Secret Clone(Secret secret)
 178    {
 195179        return new Secret
 195180        {
 195181            Id = secret.Id,
 195182            TenantId = secret.TenantId,
 195183            Name = secret.Name,
 195184            DisplayName = secret.DisplayName,
 195185            Description = secret.Description,
 195186            TypeName = secret.TypeName,
 195187            StoreName = secret.StoreName,
 195188            Scope = secret.Scope,
 195189            ManagedOwnerId = secret.ManagedOwnerId,
 195190            ManagedGenerationId = secret.ManagedGenerationId,
 195191            Tags = secret.Tags.ToHashSet(StringComparer.OrdinalIgnoreCase),
 195192            Status = secret.Status,
 195193            CreatedAt = secret.CreatedAt,
 195194            UpdatedAt = secret.UpdatedAt,
 195195            Versions = secret.Versions.Select(Clone).ToList()
 195196        };
 197    }
 198
 199    private static SecretVersion Clone(SecretVersion version)
 200    {
 127201        return new SecretVersion
 127202        {
 127203            Version = version.Version,
 127204            Status = version.Status,
 127205            CreatedAt = version.CreatedAt,
 127206            ExpiresAt = version.ExpiresAt,
 127207            Payload = new SecretPayload
 127208            {
 127209                Value = version.Payload.Value,
 127210                Metadata = new Dictionary<string, string>(version.Payload.Metadata, StringComparer.OrdinalIgnoreCase)
 127211            }
 127212        };
 213    }
 214}