< Summary

Information
Class: Elsa.Secrets.Repositories.FileSecretRepository
Assembly: Elsa.Secrets
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Secrets/Repositories/FileSecretRepository.cs
Line coverage
95%
Covered lines: 64
Uncovered lines: 3
Coverable lines: 67
Total lines: 146
Line coverage: 95.5%
Branch coverage
79%
Covered branches: 19
Total branches: 24
Branch coverage: 79.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetAsync()100%11100%
ListAsync()100%11100%
AddAsync()75%4487.5%
TryAddOrReplaceDeletedAsync()83.33%6690.9%
SaveAsync()100%44100%
ReadAllAsync()100%11100%
ReadAllUnsafeAsync()75%88100%
WriteAllUnsafeAsync()75%4488.88%
GetPath()50%22100%

File(s)

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

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Serialization;
 3using Microsoft.Extensions.Logging;
 4using Microsoft.Extensions.Options;
 5
 6namespace Elsa.Secrets.Repositories;
 7
 58public class FileSecretRepository(IOptions<SecretsOptions> options, ILogger<FileSecretRepository>? logger = null) : ISec
 9{
 510    private readonly SemaphoreSlim _lock = new(1, 1);
 511    private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web)
 512    {
 513        Converters = { new JsonStringEnumConverter() },
 514        WriteIndented = true
 515    };
 16
 17    public async Task<Secret?> GetAsync(string normalizedName, CancellationToken cancellationToken = default)
 18    {
 419        var secrets = await ReadAllAsync(cancellationToken);
 820        return secrets.FirstOrDefault(x => string.Equals(x.Name, normalizedName, StringComparison.OrdinalIgnoreCase));
 421    }
 22
 23    public async Task<IReadOnlyCollection<Secret>> ListAsync(CancellationToken cancellationToken = default)
 24    {
 125        return await ReadAllAsync(cancellationToken);
 126    }
 27
 28    public async Task AddAsync(Secret secret, CancellationToken cancellationToken = default)
 29    {
 330        await _lock.WaitAsync(cancellationToken);
 31        try
 32        {
 333            var secrets = await ReadAllUnsafeAsync(cancellationToken);
 334            if (secrets.Any(x => string.Equals(x.Name, secret.Name, StringComparison.OrdinalIgnoreCase)))
 035                throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");
 36
 337            secrets.Add(secret);
 338            await WriteAllUnsafeAsync(secrets, cancellationToken);
 339        }
 40        finally
 41        {
 342            _lock.Release();
 43        }
 344    }
 45
 46    public async Task<bool> TryAddOrReplaceDeletedAsync(Secret secret, CancellationToken cancellationToken = default)
 47    {
 248        await _lock.WaitAsync(cancellationToken);
 49        try
 50        {
 251            var secrets = await ReadAllUnsafeAsync(cancellationToken);
 452            var index = secrets.FindIndex(x => string.Equals(x.Name, secret.Name, StringComparison.OrdinalIgnoreCase));
 253            if (index >= 0)
 54            {
 255                if (secrets[index].Status != SecretStatus.Deleted)
 156                    return false;
 57
 158                secrets[index] = secret;
 59            }
 60            else
 61            {
 062                secrets.Add(secret);
 63            }
 64
 165            await WriteAllUnsafeAsync(secrets, cancellationToken);
 166            return true;
 67        }
 68        finally
 69        {
 270            _lock.Release();
 71        }
 272    }
 73
 74    public async Task SaveAsync(Secret secret, CancellationToken cancellationToken = default)
 75    {
 376        await _lock.WaitAsync(cancellationToken);
 77        try
 78        {
 379            var secrets = await ReadAllUnsafeAsync(cancellationToken);
 580            var index = secrets.FindIndex(x => string.Equals(x.Name, secret.Name, StringComparison.OrdinalIgnoreCase));
 381            if (index < 0)
 182                secrets.Add(secret);
 83            else
 284                secrets[index] = secret;
 85
 386            await WriteAllUnsafeAsync(secrets, cancellationToken);
 387        }
 88        finally
 89        {
 390            _lock.Release();
 91        }
 392    }
 93
 94    private async Task<List<Secret>> ReadAllAsync(CancellationToken cancellationToken)
 95    {
 596        await _lock.WaitAsync(cancellationToken);
 97        try
 98        {
 599            return await ReadAllUnsafeAsync(cancellationToken);
 100        }
 101        finally
 102        {
 5103            _lock.Release();
 104        }
 5105    }
 106
 107    private async Task<List<Secret>> ReadAllUnsafeAsync(CancellationToken cancellationToken)
 108    {
 13109        var path = GetPath();
 13110        if (!File.Exists(path))
 3111            return [];
 112
 10113        await using var stream = File.OpenRead(path);
 114        try
 115        {
 10116            return await JsonSerializer.DeserializeAsync<List<Secret>>(stream, _jsonOptions, cancellationToken) ?? [];
 117        }
 2118        catch (JsonException e)
 119        {
 2120            logger?.LogError(e, "The secrets repository file '{Path}' could not be read because it contains invalid JSON
 2121            return [];
 122        }
 13123    }
 124
 125    private async Task WriteAllUnsafeAsync(List<Secret> secrets, CancellationToken cancellationToken)
 126    {
 7127        var path = GetPath();
 7128        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
 129
 7130        var temporaryPath = $"{path}.{Guid.NewGuid():N}.tmp";
 131        try
 132        {
 7133            await using (var stream = File.Create(temporaryPath))
 7134                await JsonSerializer.SerializeAsync(stream, secrets.OrderBy(x => x.Name).ToList(), _jsonOptions, cancell
 135
 7136            File.Move(temporaryPath, path, true);
 7137        }
 138        finally
 139        {
 7140            if (File.Exists(temporaryPath))
 0141                File.Delete(temporaryPath);
 142        }
 7143    }
 144
 20145    private string GetPath() => options.Value.RepositoryFilePath ?? SecretsOptions.DefaultRepositoryFilePath;
 146}