< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.PersonDependentsRemoval
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/PersonDependentsRemoval.cs
Line coverage
100%
Covered lines: 31
Uncovered lines: 0
Coverable lines: 31
Total lines: 85
Line coverage: 100%
Branch coverage
75%
Covered branches: 6
Total branches: 8
Branch coverage: 75%
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
RemoveAsync()100%22100%
Collect()100%11100%
DeleteAllAsync()66.66%66100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/PersonDependentsRemoval.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Entities;
 2using ArturRios.Data.Relational.Core.Interfaces;
 3using ArturRios.Heimdall.Domain.Entities;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace ArturRios.Heimdall.Command.Services;
 7
 8/// <summary>
 9///     Permanently removes the rows that hold data belonging to persons whose identity is being
 10///     anonymised: their password reset and email verification tokens, and their two-factor
 11///     configuration — whose <c>ON DELETE CASCADE</c> foreign keys take the recovery codes and email
 12///     codes with it.
 13/// </summary>
 14/// <remarks>
 15///     Shared by the scheduled anonymisation pass (NFR-20) and the re-application of erasures after
 16///     a restore (NFR-25), so the two cannot disagree about what an erased person leaves behind. Only
 17///     the first used to remove these rows; the second anonymised the person and kept the encrypted
 18///     TOTP secret, and the scheduled pass never came back for it, since it selects only records not
 19///     yet anonymised.
 20/// </remarks>
 21public static class PersonDependentsRemoval
 22{
 23    /// <returns>How many rows the deletes reported removing, and any errors they returned.</returns>
 24    public static async Task<(int Removed, IReadOnlyList<string> Errors)> RemoveAsync(
 25        IReadOnlyCollection<long> personIds,
 26        IAsyncRepository<PasswordResetToken, long> passwordResetTokens,
 27        IAsyncRepository<EmailVerificationToken, long> emailVerificationTokens,
 28        IAsyncRepository<TwoFactorAuth, long> twoFactorAuths)
 29    {
 2430        if (personIds.Count == 0)
 31        {
 332            return (0, []);
 33        }
 34
 2135        var removed = 0;
 2136        var errors = new List<string>();
 37
 2138        var passwordResetTokenIds = await passwordResetTokens.Query()
 2139            .Where(token => personIds.Contains(token.PersonId))
 2140            .Select(token => token.Id)
 2141            .ToListAsync();
 42
 2143        var emailVerificationTokenIds = await emailVerificationTokens.Query()
 2144            .Where(token => personIds.Contains(token.PersonId))
 2145            .Select(token => token.Id)
 2146            .ToListAsync();
 47
 2148        var twoFactorAuthIds = await twoFactorAuths.Query()
 2149            .Where(configuration => personIds.Contains(configuration.PersonId))
 2150            .Select(configuration => configuration.Id)
 2151            .ToListAsync();
 52
 2153        Collect(await DeleteAllAsync(passwordResetTokenIds, passwordResetTokens));
 2154        Collect(await DeleteAllAsync(emailVerificationTokenIds, emailVerificationTokens));
 2155        Collect(await DeleteAllAsync(twoFactorAuthIds, twoFactorAuths));
 56
 2157        return (removed, errors);
 58
 59        void Collect((int Removed, IEnumerable<string> Errors) result)
 60        {
 6361            removed += result.Removed;
 6362            errors.AddRange(result.Errors);
 6363        }
 2464    }
 65
 66    /// <summary>
 67    ///     Permanently removes the entities with the given ids, or does nothing when there are none.
 68    ///     Reports how many rows were actually removed, which is what the delete reported rather
 69    ///     than what was selected.
 70    /// </summary>
 71    private static async Task<(int Removed, IEnumerable<string> Errors)> DeleteAllAsync<T>(
 72        IReadOnlyCollection<long> ids, IAsyncRepository<T, long> writer) where T : Entity<long>
 73    {
 6374        if (ids.Count == 0)
 75        {
 5476            return (0, []);
 77        }
 78
 979        var deletion = await writer.DeleteRangeAsync(ids);
 80
 981        return deletion.Success
 982            ? (deletion.Data?.Count() ?? 0, [])
 983            : (0, deletion.Errors);
 6384    }
 85}