< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.IssueApplicationCredentialCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/IssueApplicationCredentialCommandHandler.cs
Line coverage
100%
Covered lines: 34
Uncovered lines: 0
Coverable lines: 34
Total lines: 83
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
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%1010100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/IssueApplicationCredentialCommandHandler.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.Persistence;
 7using ArturRios.Heimdall.Shared.Messages;
 8using ArturRios.Heimdall.Shared.Services;
 9using ArturRios.Heimdall.Shared.Time;
 10using ArturRios.Mediator.Command.Interfaces;
 11using ArturRios.Output;
 12using FluentValidation;
 13using Application = ArturRios.Heimdall.Domain.Entities.Application;
 14
 15namespace ArturRios.Heimdall.Command.Handlers;
 16
 17/// <summary>
 18///     Handles <see cref="IssueApplicationCredentialCommand" /> (UC-53, FR-AP-10/11/12): generates a
 19///     secret, stores its hash, and returns the secret once.
 20/// </summary>
 21/// <remarks>
 22///     The limit of <see cref="ApplicationCredential.MaxLive" /> is checked and the row written by
 23///     <see cref="IApplicationCredentialSlots" /> in one locked step; checking here and inserting
 24///     through a repository would let two concurrent requests both pass.
 25/// </remarks>
 2726public class IssueApplicationCredentialCommandHandler(
 2727    IValidator<IssueApplicationCredentialCommand> validator,
 2728    IAsyncReadOnlyRepository<Application, long> applicationReader,
 2729    IApplicationCredentialSlots slots)
 30    : ICommandHandlerAsync<IssueApplicationCredentialCommand, IssueApplicationCredentialCommandOutput>
 31{
 32    public async Task<DataOutput<IssueApplicationCredentialCommandOutput?>> HandleAsync(
 33        IssueApplicationCredentialCommand command, CancellationToken cancellationToken = default)
 34    {
 2735        var output = DataOutput<IssueApplicationCredentialCommandOutput?>.New;
 36
 37        // AF-53c.
 2738        var validation = await validator.ValidateAsync(command, cancellationToken);
 39
 2740        if (!validation.IsValid)
 41        {
 442            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 43        }
 44
 45        // AF-53a, then AF-53b.
 2546        var access = await ApplicationManagementAccess.ResolveAsync(
 2547            applicationReader.Query(), command.ScopeId, command.Id, command.ActingRole, command.ActingPersonId);
 48
 2549        switch (access.Access)
 50        {
 51            case ApplicationAccess.NotFound:
 352                return output.WithError(ApplicationMessages.ApplicationNotFound);
 53            case ApplicationAccess.Forbidden:
 254                return output.WithError(ApplicationCredentialMessages.NotAuthorizedToManageCredentials);
 55        }
 56
 2057        var now = DateTime.UtcNow;
 2058        var secret = ApplicationCredentialSecret.New();
 2059        var credential = new ApplicationCredential
 2060        {
 2061            ApplicationId = access.Application!.Id,
 2062            SecretHash = ApplicationCredentialSecret.HashOf(secret),
 2063            CreatedAt = now,
 2064            ExpiresAt = command.ExpiresAt is { } expiresAt ? UtcInstant.Of(expiresAt) : null
 2065        };
 66
 67        // AF-53d.
 2068        if (!await slots.TryAddWithinLimitAsync(credential, ApplicationCredential.MaxLive, now))
 69        {
 870            return output.WithError(ApplicationCredentialMessages.CredentialLimitReached);
 71        }
 72
 1273        return output
 1274            .WithData(new IssueApplicationCredentialCommandOutput
 1275            {
 1276                Id = credential.PublicId,
 1277                Secret = secret,
 1278                CreatedAt = credential.CreatedAt,
 1279                ExpiresAt = credential.ExpiresAt
 1280            })
 1281            .WithMessage(ApplicationCredentialMessages.CredentialIssued);
 2782    }
 83}