< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.RestrictProcessingCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/RestrictProcessingCommandHandler.cs
Line coverage
96%
Covered lines: 28
Uncovered lines: 1
Coverable lines: 29
Total lines: 99
Line coverage: 96.5%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
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()85.71%141495.45%
Restricted(...)100%11100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/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.Domain.Persistence;
 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="RestrictProcessingCommand" /> (UC-44): suspends processing of the
 16///     caller's own identity without deleting anything (GDPR Art. 18, LGPD Art. 18 III–IV). All
 17///     failures are returned as errors on the <see cref="DataOutput{T}" /> rather than thrown.
 18/// </summary>
 19/// <remarks>
 20///     <para>
 21///         <b>Why this is not the deletion flag.</b> `IsDeleted` means the identity is on its way
 22///         out: it is excluded from reads, it cascades through UC-04, and NFR-20 eventually
 23///         anonymises it. A restriction means the opposite — the record is disputed and must be
 24///         preserved exactly as it stands while that is resolved. Reusing the flag would set an
 25///         erasure clock running on data the subject has specifically asked be kept.
 26///     </para>
 27///     <para>
 28///         <b>No credential is required, unlike UC-42.</b> Erasure is irreversible, so it demands
 29///         proof the person is present. A restriction destroys nothing and is liftable, and the
 30///         subject asking for one may be doing so precisely because they believe the account is
 31///         compromised — demanding the password of somebody in that position would be the wrong way
 32///         round.
 33///     </para>
 34/// </remarks>
 1635public class RestrictProcessingCommandHandler(
 1636    IAsyncReadOnlyRepository<Person, long> personReader,
 1637    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1638    IAtomicWrites atomicWrites)
 39    : ICommandHandlerAsync<RestrictProcessingCommand, RestrictProcessingCommandOutput>
 40{
 41    public async Task<DataOutput<RestrictProcessingCommandOutput?>> HandleAsync(RestrictProcessingCommand command, Cance
 42    {
 1643        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.
 1647        if (!Enum.IsDefined((RestrictionGrounds)command.Ground))
 48        {
 449            return output.WithError(ErasureMessages.RestrictionGroundUnknown);
 50        }
 51
 1252        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.
 1257        var person = await personReader.Query()
 1258            .FirstOrDefaultAsync(x => x.PublicId == command.ActingPersonId);
 59
 1260        if (person is not null)
 61        {
 862            if (person.ProcessingRestrictedAt is not null)
 63            {
 264                return output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 65            }
 66
 67            // A targeted write of the restriction's columns alone (#237), and only while there is
 68            // none: a restriction imposed since the read stands, and this one is refused as the
 69            // check above would have refused it.
 670            return await atomicWrites.TryRestrictProcessingAsync(person, command.Ground, restrictedAt)
 671                ? Restricted(output, person.PublicId, restrictedAt, command.Ground)
 672                : output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 73        }
 74
 475        var googleUser = await googleUserReader.Query()
 476            .FirstOrDefaultAsync(x => x.PublicId == command.ActingPersonId);
 77
 478        if (googleUser is null)
 79        {
 180            return output.WithError(ErasureMessages.NotEligible);
 81        }
 82
 383        if (googleUser.ProcessingRestrictedAt is not null)
 84        {
 085            return output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 86        }
 87
 88        // As for a person (#205): the restriction's columns alone, and only while there is none.
 389        return await atomicWrites.TryRestrictProcessingAsync(googleUser, command.Ground, restrictedAt)
 390            ? Restricted(output, googleUser.PublicId, restrictedAt, command.Ground)
 391            : output.WithError(ErasureMessages.ProcessingAlreadyRestricted);
 1692    }
 93
 94    private static DataOutput<RestrictProcessingCommandOutput?> Restricted(
 95        DataOutput<RestrictProcessingCommandOutput?> output, Guid id, DateTime at, int ground) =>
 796        output
 797            .WithData(new RestrictProcessingCommandOutput { Id = id, RestrictedAt = at, Ground = ground })
 798            .WithMessage(ErasureMessages.ProcessingRestricted);
 99}