< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.DiscardPendingRegistrationCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/DiscardPendingRegistrationCommandHandler.cs
Line coverage
96%
Covered lines: 27
Uncovered lines: 1
Coverable lines: 28
Total lines: 86
Line coverage: 96.4%
Branch coverage
85%
Covered branches: 12
Total branches: 14
Branch coverage: 85.7%
Method coverage
100%
Covered methods: 2
Fully covered methods: 1
Total methods: 2
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
HandleAsync()85.71%141495.65%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Data.Relational.Core.Repositories;
 3using ArturRios.Heimdall.Command.Input;
 4using ArturRios.Heimdall.Command.Output;
 5using ArturRios.Heimdall.Domain.Entities;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Heimdall.Shared.Services;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="DiscardPendingRegistrationCommand" /> (UC-50, FR-SR-12): removes a scope's
 16///     pending self-registration, so its mailed link stops working and confirming it behaves as for any
 17///     unknown token (UC-14 AF-14c). The registrant may simply sign up again.
 18/// </summary>
 19/// <remarks>
 20///     <para>
 21///         Same access rule as UC-49: a System Admin, or an owner of the scope. A missing or logically
 22///         deleted scope is AF-50a; a caller who may not manage it is AF-50b; a registration that is not
 23///         this scope's is AF-50c, indistinguishable from one that does not exist.
 24///     </para>
 25///     <para>
 26///         A delete that loses a race — the registrant confirmed, or the purge removed the row, after
 27///         the lookup — is reported as not found. The row is gone either way, and a conflict would ask
 28///         the owner to retry something there is nothing left to do.
 29///     </para>
 30/// </remarks>
 1531public class DiscardPendingRegistrationCommandHandler(
 1532    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 1533    IAsyncReadOnlyRepository<PendingRegistration, long> pendingReader,
 1534    IAsyncRepository<PendingRegistration, long> pendingWriter,
 1535    IScopeOwnershipChecker scopeOwnership)
 36    : ICommandHandlerAsync<DiscardPendingRegistrationCommand, DiscardPendingRegistrationCommandOutput>
 37{
 38    public async Task<DataOutput<DiscardPendingRegistrationCommandOutput?>> HandleAsync(
 39        DiscardPendingRegistrationCommand command, CancellationToken cancellationToken = default)
 40    {
 1541        var output = DataOutput<DiscardPendingRegistrationCommandOutput?>.New;
 42
 43        // AF-50a.
 1544        var scope = await scopeReader.Query()
 1545            .FirstOrDefaultAsync(x => x.PublicId == command.ScopeId && !x.IsDeleted, cancellationToken);
 46
 1547        if (scope is null)
 48        {
 349            return output.WithError(ScopeMessages.ScopeNotFound);
 50        }
 51
 52        // AF-50b. Before the registration lookup, so a non-owner cannot learn which ids exist.
 1253        if (!await scopeOwnership.ActorMayManageScopeAsync(command.ActingRole, command.ActingPersonId, scope.Id))
 54        {
 255            return output.WithError(ScopeMessages.NotScopeOwner);
 56        }
 57
 58        // AF-50c: looked up within the scope, so another scope's registration is simply not found.
 1059        var pending = await pendingReader.Query()
 1060            .FirstOrDefaultAsync(
 1061                x => x.PublicId == command.RegistrationId && x.ScopeId == scope.Id, cancellationToken);
 62
 1063        if (pending is null)
 64        {
 465            return output.WithError(ScopeMessages.PendingRegistrationNotFound);
 66        }
 67
 668        var removal = await pendingWriter.DeleteRangeAsync([pending.Id], cancellationToken);
 69
 670        if (!removal.Success)
 71        {
 272            return removal.Errors.Contains(RelationalErrors.ConcurrencyMessage)
 273                ? output.WithError(ScopeMessages.PendingRegistrationNotFound)
 274                : output.WithErrors(removal.Errors);
 75        }
 76
 477        if ((removal.Data?.Count() ?? 0) == 0)
 78        {
 079            return output.WithError(ScopeMessages.PendingRegistrationNotFound);
 80        }
 81
 482        return output
 483            .WithData(new DiscardPendingRegistrationCommandOutput { Id = pending.PublicId })
 484            .WithMessage(ScopeMessages.PendingRegistrationDiscarded);
 1585    }
 86}