< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.LiftProcessingRestrictionCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/LiftProcessingRestrictionCommandHandler.cs
Line coverage
98%
Covered lines: 49
Uncovered lines: 1
Coverable lines: 50
Total lines: 151
Line coverage: 98%
Branch coverage
92%
Covered branches: 26
Total branches: 28
Branch coverage: 92.8%
Method coverage
100%
Covered methods: 5
Fully covered methods: 4
Total methods: 5
Method coverage: 100%
Full method coverage: 80%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
HandleAsync()93.75%161695%
<HandleAsync()100%22100%
<HandleAsync()50%22100%
LiftAsync()100%88100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/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.Domain.Persistence;
 8using ArturRios.Heimdall.Shared.Messages;
 9using ArturRios.Mediator.Command.Interfaces;
 10using ArturRios.Output;
 11using Microsoft.EntityFrameworkCore;
 12
 13namespace ArturRios.Heimdall.Command.Handlers;
 14
 15/// <summary>
 16///     Handles <see cref="LiftProcessingRestrictionCommand" /> (UC-45): lifts a restriction, after
 17///     informing the subject where the law requires it. All failures are returned as errors on the
 18///     <see cref="DataOutput{T}" /> rather than thrown.
 19/// </summary>
 20/// <remarks>
 21///     <para>
 22///         <b>Art. 18(3) is a precondition, not a courtesy.</b> The subject "shall be informed
 23///         before the restriction is lifted", so a failed notification stops the lift. That is the
 24///         opposite of how every other delivery in this API behaves — elsewhere a failure is
 25///         deliberately not the caller's problem — and the difference is deliberate: a lift that
 26///         proceeded after a failed send would be unlawful and would look identical to one that
 27///         worked.
 28///     </para>
 29///     <para>
 30///         <b>A subject lifting their own restriction needs no notification.</b> They are the person
 31///         Art. 18(3) exists to inform. Requiring an email to be delivered to somebody who is
 32///         standing in front of you, and refusing their request when it bounces, would be the
 33///         article's letter against its purpose.
 34///     </para>
 35/// </remarks>
 1736public class LiftProcessingRestrictionCommandHandler(
 1737    IAsyncReadOnlyRepository<Person, long> personReader,
 1738    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1739    IRestrictionLiftNotifier notifier,
 1740    IAtomicWrites atomicWrites)
 41    : ICommandHandlerAsync<LiftProcessingRestrictionCommand, LiftProcessingRestrictionCommandOutput>
 42{
 43    public async Task<DataOutput<LiftProcessingRestrictionCommandOutput?>> HandleAsync(
 44        LiftProcessingRestrictionCommand command, CancellationToken cancellationToken = default)
 45    {
 1746        var output = DataOutput<LiftProcessingRestrictionCommandOutput?>.New;
 47
 1748        var subjectId = command.SubjectId ?? command.ActingPersonId;
 1749        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.
 1758        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.
 1366        if (!liftingOwn && await personReader.Query().AnyAsync(x =>
 1367                x.PublicId == command.ActingPersonId && (x.IsDeleted || x.ProcessingRestrictedAt != null)))
 68        {
 269            return output.WithError(ErasureMessages.NotEligible);
 70        }
 71
 1172        var person = await personReader.Query().FirstOrDefaultAsync(x => x.PublicId == subjectId);
 73
 1174        if (person is not null)
 75        {
 76            // A targeted write of the restriction's columns alone (#237), and only of the
 77            // restriction read: one lifted since answers AF-45a, as it would have a moment earlier,
 78            // and one imposed again since is not lifted by a notice sent about the first.
 979            return await LiftAsync(
 980                output, person.ProcessingRestrictedAt, person.Email, liftingOwn,
 781                async notifiedAt => await atomicWrites.TryLiftRestrictionAsync(person, notifiedAt, DateTime.UtcNow)
 782                    ? null
 783                    : [ErasureMessages.NotRestricted],
 984                person.PublicId);
 85        }
 86
 287        var googleUser = await googleUserReader.Query().FirstOrDefaultAsync(x => x.PublicId == subjectId);
 88
 289        if (googleUser is null)
 90        {
 091            return output.WithError(ErasureMessages.NotEligible);
 92        }
 93
 94        // As for a person (#205): the restriction's columns alone, and only of the restriction read.
 295        return await LiftAsync(
 296            output, googleUser.ProcessingRestrictedAt, googleUser.Email, liftingOwn,
 297            async notifiedAt => await atomicWrites.TryLiftRestrictionAsync(googleUser, notifiedAt, DateTime.UtcNow)
 298                ? null
 299                : [ErasureMessages.NotRestricted],
 2100            googleUser.PublicId);
 17101    }
 102
 103    /// <param name="output">The output to answer on.</param>
 104    /// <param name="restrictedAt">When the restriction read began, or <c>null</c> when there is none.</param>
 105    /// <param name="email">Where the Art. 18(3) notice goes.</param>
 106    /// <param name="liftingOwn">Whether the subject is lifting their own restriction.</param>
 107    /// <param name="lift">
 108    ///     Lifts and persists, given when the subject was told; returns the errors, or <c>null</c>.
 109    /// </param>
 110    /// <param name="subjectPublicId">The subject's public id, for the response.</param>
 111    private async Task<DataOutput<LiftProcessingRestrictionCommandOutput?>> LiftAsync(
 112        DataOutput<LiftProcessingRestrictionCommandOutput?> output,
 113        DateTime? restrictedAt,
 114        string email,
 115        bool liftingOwn,
 116        Func<DateTime?, Task<IEnumerable<string>?>> lift,
 117        Guid subjectPublicId)
 118    {
 119        // AF-45a.
 11120        if (restrictedAt is null)
 121        {
 1122            return output.WithError(ErasureMessages.NotRestricted);
 123        }
 124
 10125        DateTime? notifiedAt = null;
 126
 10127        if (!liftingOwn)
 128        {
 129            // AF-45b. Informing comes first and the lift does not proceed without it.
 5130            if (!await notifier.NotifyAsync(email))
 131            {
 1132                return output.WithError(ErasureMessages.RestrictionLiftNotificationFailed);
 133            }
 134
 4135            notifiedAt = DateTime.UtcNow;
 136        }
 137
 9138        var errors = await lift(notifiedAt);
 139
 9140        return errors is null
 9141            ? output
 9142                .WithData(new LiftProcessingRestrictionCommandOutput
 9143                {
 9144                    Id = subjectPublicId,
 9145                    LiftedAt = DateTime.UtcNow,
 9146                    SubjectNotified = notifiedAt is not null
 9147                })
 9148                .WithMessage(ErasureMessages.RestrictionLifted)
 9149            : output.WithErrors(errors);
 11150    }
 151}