< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.DeleteApplicationCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/DeleteApplicationCommandHandler.cs
Line coverage
96%
Covered lines: 26
Uncovered lines: 1
Coverable lines: 27
Total lines: 87
Line coverage: 96.2%
Branch coverage
91%
Covered branches: 11
Total branches: 12
Branch coverage: 91.6%
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()91.66%121295.65%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Command.Input;
 3using ArturRios.Heimdall.Command.Output;
 4using ArturRios.Heimdall.Domain.Enums;
 5using ArturRios.Heimdall.Shared.Messages;
 6using ArturRios.Mediator.Command.Interfaces;
 7using ArturRios.Output;
 8using Microsoft.EntityFrameworkCore;
 9using Application = ArturRios.Heimdall.Domain.Entities.Application;
 10
 11namespace ArturRios.Heimdall.Command.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="DeleteApplicationCommand" /> (UC-19, FR-AP-07): locates the application
 15///     inside the addressed scope (AF-19a), enforces the acting role's rule — a System Admin may
 16///     delete any application, anyone else only the ones they own (AF-19c) — serves an
 17///     already-deleted application as an idempotent no-op (AF-19b), and otherwise sets
 18///     <c>IsDeleted = true</c> and stamps <c>UpdatedAt</c>. Nothing cascades: an application owns no
 19///     dependent row. All failures are returned as errors on the <see cref="DataOutput{T}" /> rather
 20///     than thrown.
 21/// </summary>
 2222public class DeleteApplicationCommandHandler(
 2223    IAsyncReadOnlyRepository<Application, long> applicationReader,
 2224    IAsyncRepository<Application, long> applicationWriter)
 25    : ICommandHandlerAsync<DeleteApplicationCommand, DeleteApplicationCommandOutput>
 26{
 27    public async Task<DataOutput<DeleteApplicationCommandOutput?>> HandleAsync(DeleteApplicationCommand command, Cancell
 28    {
 2229        var output = DataOutput<DeleteApplicationCommandOutput?>.New;
 30
 31        // AF-19a: the application must exist inside the addressed scope. The lookup deliberately omits
 32        // the !IsDeleted filter UC-18 applies, so an already-deleted application is found and served
 33        // idempotently by AF-19b below rather than reported as not found. The route's scopeId
 34        // qualifies it: an application that lives in another scope is not the resource this path
 35        // addresses. Checked before authorization so AF-19a and AF-19c both stay observable.
 2236        var application = await applicationReader.Query()
 2237            .Include(x => x.Owner)
 2238            .ThenInclude(owner => owner.ScopeOwnerships)
 2239            .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId);
 40
 2241        if (application is null)
 42        {
 543            return output.WithError(ApplicationMessages.ApplicationNotFound);
 44        }
 45
 46        // AF-19c (UC-19 step 2): a System Admin may delete any application; anyone else must own it.
 47        // Owning the *scope* is not by itself grounds to delete another owner's application, so the
 48        // rule compares the owner rather than consulting IScopeOwnershipChecker — the same call UC-17
 49        // makes for reads and UC-18 for updates. Runs before AF-19b so an already-deleted application
 50        // cannot be used to probe for applications outside the caller's reach.
 51        //
 52        // An owner who no longer owns the application's scope (UC-22 removed them) is not a valid
 53        // owner under FR-AP-03, and is refused as UC-17 refuses them.
 1754        if (command.ActingRole != (int)Roles.SystemAdmin &&
 1755            (application.Owner.PublicId != command.ActingPersonId ||
 2256             application.Owner.ScopeOwnerships.All(ownership => ownership.ScopeId != application.ScopeId)))
 57        {
 558            return output.WithError(ApplicationMessages.NotAuthorizedToDeleteApplication);
 59        }
 60
 61        // AF-19b: already deleted — whether directly or by UC-04's cascade from its scope — so there
 62        // is nothing to write. UpdatedAt is left alone: the row already carries the requested state,
 63        // and re-stamping it would misreport when the deletion happened.
 1264        if (application.IsDeleted)
 65        {
 566            return output
 567                .WithData(new DeleteApplicationCommandOutput { Id = application.PublicId, AlreadyDeleted = true })
 568                .WithMessage(ApplicationMessages.ApplicationDeletedSuccessfully);
 69        }
 70
 71        // UC-19 step 3: flip the flag and stamp UpdatedAt (no DB trigger maintains it).
 772        application.IsDeleted = true;
 773        application.UpdatedAt = DateTime.UtcNow;
 74
 775        var update = await applicationWriter.UpdateAsync(application);
 76
 777        if (!update.Success)
 78        {
 079            return output.WithErrors(update.Errors);
 80        }
 81
 82        // UC-19 step 4.
 783        return output
 784            .WithData(new DeleteApplicationCommandOutput { Id = application.PublicId, AlreadyDeleted = false })
 785            .WithMessage(ApplicationMessages.ApplicationDeletedSuccessfully);
 2286    }
 87}