< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.CreateApplicationCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/CreateApplicationCommandHandler.cs
Line coverage
97%
Covered lines: 45
Uncovered lines: 1
Coverable lines: 46
Total lines: 110
Line coverage: 97.8%
Branch coverage
92%
Covered branches: 13
Total branches: 14
Branch coverage: 92.8%
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()92.85%141497.43%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/CreateApplicationCommandHandler.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.Heimdall.Shared.Services;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using FluentValidation;
 11using Microsoft.EntityFrameworkCore;
 12using Application = ArturRios.Heimdall.Domain.Entities.Application;
 13
 14namespace ArturRios.Heimdall.Command.Handlers;
 15
 16/// <summary>
 17///     Handles <see cref="CreateApplicationCommand" /> (UC-16, FR-AP-01/02/03): validates input
 18///     (AF-16d), verifies the target scope exists and is active (AF-16a), enforces the acting role's
 19///     rule — a Scope Admin must own the scope (AF-16e) and may only name themself as owner
 20///     (AF-16c), a System Admin bypasses both — verifies the owner is a non-deleted
 21///     <c>ScopeAdmin</c> who owns the scope (AF-16b), then creates the application record.
 22/// </summary>
 3023public class CreateApplicationCommandHandler(
 3024    IValidator<CreateApplicationCommand> validator,
 3025    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 3026    IAsyncReadOnlyRepository<Person, long> personReader,
 3027    IAsyncRepository<Application, long> applicationWriter,
 3028    IScopeOwnershipChecker scopeOwnership)
 29    : ICommandHandlerAsync<CreateApplicationCommand, CreateApplicationCommandOutput>
 30{
 31    public async Task<DataOutput<CreateApplicationCommandOutput?>> HandleAsync(CreateApplicationCommand command, Cancell
 32    {
 3033        var output = DataOutput<CreateApplicationCommandOutput?>.New;
 34
 35        // AF-16d: validate input shape.
 3036        var validation = await validator.ValidateAsync(command);
 37
 3038        if (!validation.IsValid)
 39        {
 440            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 41        }
 42
 43        // AF-16a: the target scope must exist and not be logically deleted.
 2844        var scope = await scopeReader.Query()
 2845            .FirstOrDefaultAsync(x => x.PublicId == command.ScopeId && !x.IsDeleted);
 46
 2847        if (scope is null)
 48        {
 549            return output.WithError(ApplicationMessages.ScopeNotFound);
 50        }
 51
 52        // AF-16e: a Scope Admin actor may only act on a scope they own; a System Admin bypasses the
 53        // check inside the checker. Asked before who was named, so an actor acting where they have no
 54        // standing is told that, rather than being told about the owner.
 2355        if (!await scopeOwnership.ActorMayManageScopeAsync(
 2356                command.ActingRole, command.ActingPersonId, scope.Id))
 57        {
 258            return output.WithError(ApplicationMessages.NotScopeOwner);
 59        }
 60
 61        // AF-16c: a Scope Admin may only create applications they own. Decided from the command
 62        // alone, before the owner is read, so the refusal cannot reveal whether the named person
 63        // exists. A System Admin may name any owner FR-AP-03 accepts.
 2164        if (command.ActingRole != (int)Roles.SystemAdmin && command.OwnerId != command.ActingPersonId)
 65        {
 266            return output.WithError(ApplicationMessages.CannotSetAnotherOwner);
 67        }
 68
 69        // AF-16b / FR-AP-03: the owner must be an existing, non-logically-deleted administrator — a
 70        // ScopeAdmin or a SystemAdmin — who owns the scope. The role is asserted alongside the
 71        // SCOPE_OWNER row: SRD §4.5 already restricts that table to administrators, so this states
 72        // the requirement rather than adding to it.
 1973        var owner = await personReader.Query().FirstOrDefaultAsync(person =>
 1974            person.PublicId == command.OwnerId && !person.IsDeleted &&
 1975            (person.RoleId == (long)Roles.ScopeAdmin || person.RoleId == (long)Roles.SystemAdmin) &&
 1976            person.ScopeOwnerships.Any(ownership => ownership.ScopeId == scope.Id));
 77
 1978        if (owner is null)
 79        {
 980            return output.WithError(ApplicationMessages.OwnerNotValidForScope);
 81        }
 82
 83        // FR-AP-01/02: create the application in the scope, active.
 1084        var application = new Application
 1085        {
 1086            Name = command.Name,
 1087            IsDeleted = false,
 1088            ScopeId = scope.Id,
 1089            OwnerId = owner.Id
 1090        };
 91
 1092        var creation = await applicationWriter.CreateAsync(application);
 93
 1094        if (!creation.Success)
 95        {
 096            return output.WithErrors(creation.Errors);
 97        }
 98
 1099        return output
 10100            .WithData(new CreateApplicationCommandOutput
 10101            {
 10102                Id = application.PublicId,
 10103                Name = application.Name,
 10104                ScopeId = scope.PublicId,
 10105                OwnerId = owner.PublicId,
 10106                CreatedAt = application.CreatedAt
 10107            })
 10108            .WithMessage(ApplicationMessages.ApplicationCreatedSuccessfully);
 30109    }
 110}