< Summary - Heimdall API

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

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/RevokeApplicationCredentialCommandHandler.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.Persistence;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Heimdall.Shared.Services;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using Microsoft.EntityFrameworkCore;
 11using Application = ArturRios.Heimdall.Domain.Entities.Application;
 12
 13namespace ArturRios.Heimdall.Command.Handlers;
 14
 15/// <summary>
 16///     Handles <see cref="RevokeApplicationCredentialCommand" /> (UC-55, FR-AP-12): stamps
 17///     <c>RevokedAt</c>. From then on UC-56 refuses the secret and UC-57 refuses every token already
 18///     issued from it — revocation takes effect at the next live re-check, not at token expiry.
 19/// </summary>
 20/// <remarks>
 21///     The stamp is one conditional column (<see cref="IAtomicWrites.TryRevokeCredentialAsync" />), not
 22///     a whole-row save: of two revocations that both read the credential live, only the first writes,
 23///     and the second answers AF-55d rather than overwriting the first one's time.
 24/// </remarks>
 1725public class RevokeApplicationCredentialCommandHandler(
 1726    IAsyncReadOnlyRepository<Application, long> applicationReader,
 1727    IAsyncReadOnlyRepository<ApplicationCredential, long> credentialReader,
 1728    IAtomicWrites atomicWrites)
 29    : ICommandHandlerAsync<RevokeApplicationCredentialCommand, RevokeApplicationCredentialCommandOutput>
 30{
 31    public async Task<DataOutput<RevokeApplicationCredentialCommandOutput?>> HandleAsync(
 32        RevokeApplicationCredentialCommand command, CancellationToken cancellationToken = default)
 33    {
 1734        var output = DataOutput<RevokeApplicationCredentialCommandOutput?>.New;
 35
 36        // AF-55a, then AF-55b.
 1737        var access = await ApplicationManagementAccess.ResolveAsync(
 1738            applicationReader.Query(), command.ScopeId, command.Id, command.ActingRole, command.ActingPersonId);
 39
 1740        switch (access.Access)
 41        {
 42            case ApplicationAccess.NotFound:
 243                return output.WithError(ApplicationMessages.ApplicationNotFound);
 44            case ApplicationAccess.Forbidden:
 245                return output.WithError(ApplicationCredentialMessages.NotAuthorizedToManageCredentials);
 46        }
 47
 48        // AF-55c: the credential must belong to this application; one of another is not the
 49        // resource this route addresses.
 1350        var applicationId = access.Application!.Id;
 1351        var credential = await credentialReader.Query()
 1352            .FirstOrDefaultAsync(x => x.PublicId == command.CredentialId && x.ApplicationId == applicationId,
 1353                cancellationToken);
 54
 1355        if (credential is null)
 56        {
 357            return output.WithError(ApplicationCredentialMessages.CredentialNotFound);
 58        }
 59
 60        // AF-55d: the first revocation's time stands — decided by the write, not by what was read,
 61        // so a revocation committed after the read above is not overwritten.
 1062        var revokedAt = DateTime.UtcNow;
 63
 1064        if (!await atomicWrites.TryRevokeCredentialAsync(credential, revokedAt))
 65        {
 466            return output.WithError(ApplicationCredentialMessages.CredentialAlreadyRevoked);
 67        }
 68
 669        return output
 670            .WithData(new RevokeApplicationCredentialCommandOutput
 671            {
 672                Id = credential.PublicId,
 673                RevokedAt = revokedAt
 674            })
 675            .WithMessage(ApplicationCredentialMessages.CredentialRevoked);
 1776    }
 77}