< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.RegisterCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/RegisterCommandHandler.cs
Line coverage
100%
Covered lines: 20
Uncovered lines: 0
Coverable lines: 20
Total lines: 71
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage
100%
Covered methods: 2
Fully covered methods: 2
Total methods: 2
Method coverage: 100%
Full method coverage: 100%

Metrics

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

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/RegisterCommandHandler.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.Shared.Messages;
 7using ArturRios.Mediator.Command.Interfaces;
 8using ArturRios.Output;
 9using FluentValidation;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="RegisterCommand" /> (UC-47): validates, confirms the scope is open, hashes the
 16///     password, queues the request and answers. Whether the address is free — and so whether a person
 17///     is created or the existing owner is told of the attempt — is decided afterwards, in
 18///     <see cref="RegistrationProcessor" />.
 19/// </summary>
 20/// <remarks>
 21///     <para>
 22///         Every valid request against an open scope gets the same answer in the same time (AF-47b). The
 23///         scope check is on the request path because it says nothing about any address: a closed
 24///         scope is closed for everyone.
 25///     </para>
 26///     <para>
 27///         The password is hashed here rather than in the processor so that the queue never holds a
 28///         plaintext secret, and so that the request path costs one hash for every caller.
 29///     </para>
 30/// </remarks>
 3231public class RegisterCommandHandler(
 3232    IValidator<RegisterCommand> validator,
 3233    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 3234    IRegistrationQueue registrationQueue)
 35    : ICommandHandlerAsync<RegisterCommand, RegisterCommandOutput>
 36{
 37    public async Task<DataOutput<RegisterCommandOutput?>> HandleAsync(
 38        RegisterCommand command, CancellationToken cancellationToken = default)
 39    {
 3240        var output = DataOutput<RegisterCommandOutput?>.New;
 41
 42        // AF-47c: malformed input, refused on the request alone.
 3243        var validation = await validator.ValidateAsync(command, cancellationToken);
 44
 3245        if (!validation.IsValid)
 46        {
 447            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 48        }
 49
 50        // AF-47a: missing, deleted and closed scopes answer alike.
 3051        var scopeOpen = await scopeReader.Query().AnyAsync(
 3052            scope => scope.PublicId == command.ScopeId && !scope.IsDeleted && scope.SelfRegistrationEnabled,
 3053            cancellationToken);
 54
 3055        if (!scopeOpen)
 56        {
 657            return output.WithError(AuthMessages.RegistrationUnavailable);
 58        }
 59
 2460        var (passwordHash, salt) = await PasswordHashGate.Shared.EncodeWithRandomSaltAsync(command.Password);
 61
 62        // UC-47 steps 4 onward, deferred. The result is deliberately ignored: a dropped request must
 63        // answer exactly as a queued one does.
 2464        registrationQueue.TryEnqueue(new RegistrationRequest(
 2465            command.ScopeId!.Value, command.Name.Trim(), command.Email.Trim(), passwordHash, salt));
 66
 2467        return output
 2468            .WithData(new RegisterCommandOutput())
 2469            .WithMessage(AuthMessages.RegistrationRequested);
 3270    }
 71}