< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.LiftProcessingRestrictionCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/LiftProcessingRestrictionCommandHandler.cs
Line coverage
98%
Covered lines: 61
Uncovered lines: 1
Coverable lines: 62
Total lines: 152
Line coverage: 98.3%
Branch coverage
91%
Covered branches: 22
Total branches: 24
Branch coverage: 91.6%
Method coverage
100%
Covered methods: 3
Fully covered methods: 2
Total methods: 3
Method coverage: 100%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
HandleAsync()93.75%161695.83%
LiftAsync()87.5%88100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/LiftProcessingRestrictionCommandHandler.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Command.Input;
 3using ArturRios.Heimdall.Command.Output;
 4using ArturRios.Heimdall.Command.Services;
 5using ArturRios.Heimdall.Domain.Entities;
 6using ArturRios.Heimdall.Domain.Enums;
 7using ArturRios.Heimdall.Shared.Messages;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="LiftProcessingRestrictionCommand" /> (UC-45): lifts a restriction, after
 16///     informing the subject where the law requires it. All failures are returned as errors on the
 17///     <see cref="DataOutput{T}" /> rather than thrown.
 18/// </summary>
 19/// <remarks>
 20///     <para>
 21///         <b>Art. 18(3) is a precondition, not a courtesy.</b> The subject "shall be informed
 22///         before the restriction is lifted", so a failed notification stops the lift. That is the
 23///         opposite of how every other delivery in this API behaves — elsewhere a failure is
 24///         deliberately not the caller's problem — and the difference is deliberate: a lift that
 25///         proceeded after a failed send would be unlawful and would look identical to one that
 26///         worked.
 27///     </para>
 28///     <para>
 29///         <b>A subject lifting their own restriction needs no notification.</b> They are the person
 30///         Art. 18(3) exists to inform. Requiring an email to be delivered to somebody who is
 31///         standing in front of you, and refusing their request when it bounces, would be the
 32///         article's letter against its purpose.
 33///     </para>
 34/// </remarks>
 1535public class LiftProcessingRestrictionCommandHandler(
 1536    IAsyncReadOnlyRepository<Person, long> personReader,
 1537    IAsyncRepository<Person, long> personWriter,
 1538    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1539    IAsyncRepository<GoogleUser, long> googleUserWriter,
 1540    IRestrictionLiftNotifier notifier)
 41    : ICommandHandlerAsync<LiftProcessingRestrictionCommand, LiftProcessingRestrictionCommandOutput>
 42{
 43    public async Task<DataOutput<LiftProcessingRestrictionCommandOutput?>> HandleAsync(
 44        LiftProcessingRestrictionCommand command, CancellationToken cancellationToken = default)
 45    {
 1546        var output = DataOutput<LiftProcessingRestrictionCommandOutput?>.New;
 47
 1548        var subjectId = command.SubjectId ?? command.ActingPersonId;
 1549        var liftingOwn = subjectId == command.ActingPersonId;
 50
 51        // Only a System Admin may lift somebody else's. The endpoint carries no role requirement,
 52        // because a subject of any role may lift their own, so the rule is data-dependent and lives
 53        // here.
 54        //
 55        // A subject-rights-only token acts as a subject and never as an administrator (AF-45c),
 56        // including after its own restriction was lifted: it was issued to a restricted identity,
 57        // and that is all it ever stands for.
 1558        if (!liftingOwn && (command.ActingRole != (int)Roles.SystemAdmin || command.ActingSubjectRightsOnly))
 59        {
 460            return output.WithError(ErasureMessages.NotEligible);
 61        }
 62
 63        // The endpoint admits a restricted or suspended caller, because lifting their own restriction
 64        // is a right of theirs (NFR-24). Lifting somebody else's is not: an administrator who is
 65        // restricted or suspended themselves acts here as a subject, not as an administrator.
 1166        if (!liftingOwn && await personReader.Query().AnyAsync(x =>
 1167                x.PublicId == command.ActingPersonId && (x.IsDeleted || x.ProcessingRestrictedAt != null)))
 68        {
 269            return output.WithError(ErasureMessages.NotEligible);
 70        }
 71
 972        var person = await personReader.Query().FirstOrDefaultAsync(x => x.PublicId == subjectId);
 73
 974        if (person is not null)
 75        {
 876            return await LiftAsync(
 877                output, person.ProcessingRestrictedAt, person.Email, liftingOwn,
 878                notifiedAt =>
 879                {
 680                    person.ProcessingRestrictedAt = null;
 681                    person.RestrictionGround = null;
 682                    person.RestrictionLiftNotifiedAt = notifiedAt;
 683                    person.UpdatedAt = DateTime.UtcNow;
 684                },
 685                () => personWriter.UpdateAsync(person),
 886                person.PublicId);
 87        }
 88
 189        var googleUser = await googleUserReader.Query().FirstOrDefaultAsync(x => x.PublicId == subjectId);
 90
 191        if (googleUser is null)
 92        {
 093            return output.WithError(ErasureMessages.NotEligible);
 94        }
 95
 196        return await LiftAsync(
 197            output, googleUser.ProcessingRestrictedAt, googleUser.Email, liftingOwn,
 198            notifiedAt =>
 199            {
 1100                googleUser.ProcessingRestrictedAt = null;
 1101                googleUser.RestrictionGround = null;
 1102                googleUser.RestrictionLiftNotifiedAt = notifiedAt;
 1103                googleUser.UpdatedAt = DateTime.UtcNow;
 1104            },
 1105            () => googleUserWriter.UpdateAsync(googleUser),
 1106            googleUser.PublicId);
 15107    }
 108
 109    private async Task<DataOutput<LiftProcessingRestrictionCommandOutput?>> LiftAsync<T>(
 110        DataOutput<LiftProcessingRestrictionCommandOutput?> output,
 111        DateTime? restrictedAt,
 112        string email,
 113        bool liftingOwn,
 114        Action<DateTime?> clear,
 115        Func<Task<DataOutput<T>>> save,
 116        Guid subjectPublicId)
 117    {
 118        // AF-45a.
 9119        if (restrictedAt is null)
 120        {
 1121            return output.WithError(ErasureMessages.NotRestricted);
 122        }
 123
 8124        DateTime? notifiedAt = null;
 125
 8126        if (!liftingOwn)
 127        {
 128            // AF-45b. Informing comes first and the lift does not proceed without it.
 4129            if (!await notifier.NotifyAsync(email))
 130            {
 1131                return output.WithError(ErasureMessages.RestrictionLiftNotificationFailed);
 132            }
 133
 3134            notifiedAt = DateTime.UtcNow;
 135        }
 136
 7137        clear(notifiedAt);
 138
 7139        var update = await save();
 140
 7141        return update.Success
 7142            ? output
 7143                .WithData(new LiftProcessingRestrictionCommandOutput
 7144                {
 7145                    Id = subjectPublicId,
 7146                    LiftedAt = DateTime.UtcNow,
 7147                    SubjectNotified = notifiedAt is not null
 7148                })
 7149                .WithMessage(ErasureMessages.RestrictionLifted)
 7150            : output.WithErrors(update.Errors);
 9151    }
 152}