< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.UpdateScopePermissionCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/UpdateScopePermissionCommandHandler.cs
Line coverage
97%
Covered lines: 37
Uncovered lines: 1
Coverable lines: 38
Total lines: 93
Line coverage: 97.3%
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.87%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/UpdateScopePermissionCommandHandler.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 FluentValidation;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="UpdateScopePermissionCommand" /> (UC-33, FR-SP-06): validates input,
 16///     loads the permission inside the addressed scope (AF-33a), enforces the acting role's rule — a
 17///     System Admin may update any permission, anyone else only one whose scope they own (AF-33e) —
 18///     then applies the changes and stamps <c>UpdatedAt</c>. All failures are returned as errors on
 19///     the output rather than thrown.
 20/// </summary>
 2021public class UpdateScopePermissionCommandHandler(
 2022    IValidator<UpdateScopePermissionCommand> validator,
 2023    IAsyncReadOnlyRepository<ScopePermission, long> permissionReader,
 2024    IAsyncRepository<ScopePermission, long> permissionWriter,
 2025    IScopeOwnershipChecker scopeOwnership)
 26    : ICommandHandlerAsync<UpdateScopePermissionCommand, UpdateScopePermissionCommandOutput>
 27{
 28    public async Task<DataOutput<UpdateScopePermissionCommandOutput?>> HandleAsync(
 29        UpdateScopePermissionCommand command, CancellationToken cancellationToken = default)
 30    {
 2031        var output = DataOutput<UpdateScopePermissionCommandOutput?>.New;
 32
 33        // UC-33 step 2: validate input shape. UC-33 defines no alternative flow for this, so the
 34        // validator's UC-31 messages carry their existing 400.
 2035        var validation = await validator.ValidateAsync(command);
 36
 2037        if (!validation.IsValid)
 38        {
 639            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 40        }
 41
 42        // AF-33a: the permission must exist inside the addressed scope and not be logically deleted.
 43        // The route's scopeId qualifies the lookup, as UC-32's by-id read does: a permission that
 44        // lives in another scope is not the resource this path addresses. Checked before
 45        // authorization so AF-33a and AF-33e both stay observable.
 1746        var permission = await permissionReader.Query()
 1747            .Include(x => x.Scope)
 1748            .FirstOrDefaultAsync(x =>
 1749                x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId && !x.IsDeleted);
 50
 1751        if (permission is null)
 52        {
 753            return output.WithError(ScopePermissionMessages.ScopePermissionNotFound);
 54        }
 55
 56        // AF-33e: a System Admin may update any scope's permission; anyone else must own the scope.
 57        // A scope permission carries no separate owner, so the scope-ownership check is the only
 58        // authorization. Runs before any mutation so AF-33e stays observable on otherwise-no-op
 59        // updates.
 1060        if (!await scopeOwnership.ActorMayManageScopeAsync(
 1061                command.ActingRole, command.ActingPersonId, permission.ScopeId))
 62        {
 463            return output.WithError(ScopePermissionMessages.NotScopeOwner);
 64        }
 65
 66        // UC-33 step 5: apply the updates and stamp UpdatedAt (no DB trigger maintains it).
 667        permission.Name = command.Name;
 668        permission.Description = command.Description;
 669        permission.IncludeAsJwtClaim = command.IncludeAsJwtClaim;
 670        permission.UpdatedAt = DateTime.UtcNow;
 71
 672        var update = await permissionWriter.UpdateAsync(permission);
 73
 674        if (!update.Success)
 75        {
 076            return output.WithErrors(update.Errors);
 77        }
 78
 79        // UC-33 step 6: return the updated permission.
 680        return output
 681            .WithData(new UpdateScopePermissionCommandOutput
 682            {
 683                Id = permission.PublicId,
 684                Name = permission.Name,
 685                Description = permission.Description,
 686                IncludeAsJwtClaim = permission.IncludeAsJwtClaim,
 687                ScopeId = permission.Scope.PublicId,
 688                CreatedAt = permission.CreatedAt,
 689                UpdatedAt = permission.UpdatedAt
 690            })
 691            .WithMessage(ScopePermissionMessages.ScopePermissionUpdatedSuccessfully);
 2092    }
 93}