< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.DeleteScopePermissionCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/DeleteScopePermissionCommandHandler.cs
Line coverage
97%
Covered lines: 33
Uncovered lines: 1
Coverable lines: 34
Total lines: 91
Line coverage: 97%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
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()87.5%8896.66%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/DeleteScopePermissionCommandHandler.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.Shared.Messages;
 6using ArturRios.Heimdall.Shared.Services;
 7using ArturRios.Mediator.Command.Interfaces;
 8using ArturRios.Output;
 9using Microsoft.EntityFrameworkCore;
 10
 11namespace ArturRios.Heimdall.Command.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="DeleteScopePermissionCommand" /> (UC-34, FR-SP-07): locates the permission
 15///     inside the addressed scope (AF-34a), enforces the acting role's rule — a System Admin may
 16///     delete any permission, anyone else only one whose scope they own (AF-34e) — serves an
 17///     already-deleted permission as an idempotent no-op (AF-34b), and otherwise sets
 18///     <c>IsDeleted = true</c> and stamps <c>UpdatedAt</c>. Nothing cascades: a scope permission owns
 19///     no dependent row. All failures are returned as errors on the <see cref="DataOutput{T}" />
 20///     rather than thrown.
 21/// </summary>
 1922public class DeleteScopePermissionCommandHandler(
 1923    IAsyncReadOnlyRepository<ScopePermission, long> permissionReader,
 1924    IAsyncRepository<ScopePermission, long> permissionWriter,
 1925    IScopeOwnershipChecker scopeOwnership)
 26    : ICommandHandlerAsync<DeleteScopePermissionCommand, DeleteScopePermissionCommandOutput>
 27{
 28    public async Task<DataOutput<DeleteScopePermissionCommandOutput?>> HandleAsync(
 29        DeleteScopePermissionCommand command, CancellationToken cancellationToken = default)
 30    {
 1931        var output = DataOutput<DeleteScopePermissionCommandOutput?>.New;
 32
 33        // AF-34a: the permission must exist inside the addressed scope. The lookup deliberately omits
 34        // the !IsDeleted filter UC-33 applies, so an already-deleted permission is found and served
 35        // idempotently by AF-34b below rather than reported as not found. The route's scopeId
 36        // qualifies it: a permission that lives in another scope is not the resource this path
 37        // addresses. Checked before authorization so AF-34a and AF-34e both stay observable.
 1938        var permission = await permissionReader.Query()
 1939            .Include(x => x.Scope)
 1940            .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId);
 41
 1942        if (permission is null)
 43        {
 544            return output.WithError(ScopePermissionMessages.ScopePermissionNotFound);
 45        }
 46
 47        // AF-34e: a System Admin may delete any scope's permission; anyone else must own the scope.
 48        // Runs before AF-34b so an already-deleted permission cannot be used to probe for scopes the
 49        // caller may not act on.
 1450        if (!await scopeOwnership.ActorMayManageScopeAsync(
 1451                command.ActingRole, command.ActingPersonId, permission.ScopeId))
 52        {
 353            return output.WithError(ScopePermissionMessages.NotScopeOwner);
 54        }
 55
 56        // AF-34b: already deleted by an earlier call, so there is nothing to write. It cannot have
 57        // arrived here from its scope: logically deleting a scope (UC-04) does not cascade to its
 58        // permissions. UpdatedAt is left alone — the row already carries the requested state, and
 59        // re-stamping it would misreport when the deletion happened.
 1160        if (permission.IsDeleted)
 61        {
 562            return output
 563                .WithData(new DeleteScopePermissionCommandOutput
 564                {
 565                    Id = permission.PublicId,
 566                    AlreadyDeleted = true
 567                })
 568                .WithMessage(ScopePermissionMessages.ScopePermissionDeletedSuccessfully);
 69        }
 70
 71        // UC-34 step 3: flip the flag and stamp UpdatedAt (no DB trigger maintains it).
 672        permission.IsDeleted = true;
 673        permission.UpdatedAt = DateTime.UtcNow;
 74
 675        var update = await permissionWriter.UpdateAsync(permission);
 76
 677        if (!update.Success)
 78        {
 079            return output.WithErrors(update.Errors);
 80        }
 81
 82        // UC-34 step 4.
 683        return output
 684            .WithData(new DeleteScopePermissionCommandOutput
 685            {
 686                Id = permission.PublicId,
 687                AlreadyDeleted = false
 688            })
 689            .WithMessage(ScopePermissionMessages.ScopePermissionDeletedSuccessfully);
 1990    }
 91}