< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.RestrictProcessingCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/RestrictProcessingCommandHandler.cs
Line coverage
97%
Covered lines: 39
Uncovered lines: 1
Coverable lines: 40
Total lines: 109
Line coverage: 97.5%
Branch coverage
78%
Covered branches: 11
Total branches: 14
Branch coverage: 78.5%
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()78.57%141496.87%
Restricted(...)100%11100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Command.Input;
 3using ArturRios.Heimdall.Command.Output;
 4using ArturRios.Heimdall.Domain.Entities;
 5using ArturRios.Heimdall.Domain.Enums;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Mediator.Command.Interfaces;
 8using ArturRios.Output;
 9using Microsoft.EntityFrameworkCore;
 10
 11namespace ArturRios.Heimdall.Command.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="RestrictProcessingCommand" /> (UC-44): suspends processing of the
 15///     caller's own identity without deleting anything (GDPR Art. 18, LGPD Art. 18 III–IV). All
 16///     failures are returned as errors on the <see cref="DataOutput{T}" /> rather than thrown.
 17/// </summary>
 18/// <remarks>
 19///     <para>
 20///         <b>Why this is not the deletion flag.</b> `IsDeleted` means the identity is on its way
 21///         out: it is excluded from reads, it cascades through UC-04, and NFR-20 eventually
 22///         anonymises it. A restriction means the opposite — the record is disputed and must be
 23///         preserved exactly as it stands while that is resolved. Reusing the flag would set an
 24///         erasure clock running on data the subject has specifically asked be kept.
 25///     </para>
 26///     <para>
 27///         <b>No credential is required, unlike UC-42.</b> Erasure is irreversible, so it demands
 28///         proof the person is present. A restriction destroys nothing and is liftable, and the
 29///         subject asking for one may be doing so precisely because they believe the account is
 30///         compromised — demanding the password of somebody in that position would be the wrong way
 31///         round.
 32///     </para>
 33/// </remarks>
 1334public class RestrictProcessingCommandHandler(
 1335    IAsyncReadOnlyRepository<Person, long> personReader,
 1336    IAsyncRepository<Person, long> personWriter,
 1337    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1338    IAsyncRepository<GoogleUser, long> googleUserWriter)
 39    : ICommandHandlerAsync<RestrictProcessingCommand, RestrictProcessingCommandOutput>
 40{
 41    public async Task<DataOutput<RestrictProcessingCommandOutput?>> HandleAsync(RestrictProcessingCommand command, Cance
 42    {
 1343        var output = DataOutput<RestrictProcessingCommandOutput?>.New;
 44
 45        // AF-44b. Art. 18(1) is exhaustive: a restriction rests on one of its four grounds or it is
 46        // not a restriction under the article.
 1347        if (!Enum.IsDefined((RestrictionGrounds)command.Ground))
 48        {
 449            return output.WithError(ErasureMessages.RestrictionGroundUnknown);
 50        }
 51
 952        var restrictedAt = DateTime.UtcNow;
 53
 54        // The lookups omit !IsDeleted deliberately: a suspended identity may still contest the
 55        // accuracy of what is held about it, and Art. 18 does not require the account to be in good
 56        // standing. What it must not be is anonymised, and an anonymised record cannot authenticate.
 957        var person = await personReader.Query()
 958            .FirstOrDefaultAsync(x => x.PublicId == command.ActingPersonId);
 59
 960        if (person is not null)
 61        {
 762            if (person.ProcessingRestrictedAt is not null)
 63            {
 264                return output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 65            }
 66
 567            person.ProcessingRestrictedAt = restrictedAt;
 568            person.RestrictionGround = command.Ground;
 569            person.RestrictionLiftNotifiedAt = null;
 570            person.UpdatedAt = restrictedAt;
 71
 572            var update = await personWriter.UpdateAsync(person);
 73
 574            return update.Success
 575                ? Restricted(output, person.PublicId, restrictedAt, command.Ground)
 576                : output.WithErrors(update.Errors);
 77        }
 78
 279        var googleUser = await googleUserReader.Query()
 280            .FirstOrDefaultAsync(x => x.PublicId == command.ActingPersonId);
 81
 282        if (googleUser is null)
 83        {
 184            return output.WithError(ErasureMessages.NotEligible);
 85        }
 86
 187        if (googleUser.ProcessingRestrictedAt is not null)
 88        {
 089            return output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 90        }
 91
 192        googleUser.ProcessingRestrictedAt = restrictedAt;
 193        googleUser.RestrictionGround = command.Ground;
 194        googleUser.RestrictionLiftNotifiedAt = null;
 195        googleUser.UpdatedAt = restrictedAt;
 96
 197        var googleUpdate = await googleUserWriter.UpdateAsync(googleUser);
 98
 199        return googleUpdate.Success
 1100            ? Restricted(output, googleUser.PublicId, restrictedAt, command.Ground)
 1101            : output.WithErrors(googleUpdate.Errors);
 13102    }
 103
 104    private static DataOutput<RestrictProcessingCommandOutput?> Restricted(
 105        DataOutput<RestrictProcessingCommandOutput?> output, Guid id, DateTime at, int ground) =>
 6106        output
 6107            .WithData(new RestrictProcessingCommandOutput { Id = id, RestrictedAt = at, Ground = ground })
 6108            .WithMessage(ErasureMessages.ProcessingRestricted);
 109}