< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Controllers.ApplicationCredentialController
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ApplicationCredentialController.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 87
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Issue()100%22100%
List()100%11100%
Revoke()100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Controllers/ApplicationCredentialController.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Input;
 2using ArturRios.Heimdall.Command.Output;
 3using ArturRios.Heimdall.Domain.Enums;
 4using ArturRios.Heimdall.Query.Input;
 5using ArturRios.Heimdall.Query.Output;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Heimdall.WebApi.Security;
 8using ArturRios.Mediator.Command;
 9using ArturRios.Mediator.Query;
 10using ArturRios.Output;
 11using ArturRios.Util.WebApi.AspNetCore;
 12using ArturRios.Util.WebApi.Security.Attributes;
 13using Microsoft.AspNetCore.Mvc;
 14using Microsoft.AspNetCore.Mvc.ModelBinding;
 15
 16namespace ArturRios.Heimdall.WebApi.Controllers;
 17
 18/// <summary>
 19///     An application's credentials (UC-53…UC-55, FR-AP-10…FR-AP-12): the secrets it exchanges for an
 20///     application token at <c>POST /api/applications/token</c>.
 21/// </summary>
 22/// <remarks>
 23///     A <c>User</c> can own no application (FR-AP-03) and is refused by the attribute. Among System
 24///     Admins and Scope Admins the rule is data-dependent and lives in the handlers, through
 25///     <c>ApplicationManagementAccess</c> — the same rule as reading or renaming the application.
 26/// </remarks>
 27[Route("api/scopes/{scopeId:guid}/applications/{id:guid}/credentials")]
 3828public class ApplicationCredentialController(CommandMediator commandMediator, QueryMediator queryMediator) : Controller
 29{
 30    /// <summary>
 31    ///     Issues a credential (UC-53). The body is optional and carries only <c>expiresAt</c>. The
 32    ///     answer is the only place the secret ever appears; Heimdall keeps its hash alone.
 33    /// </summary>
 34    [HttpPost]
 35    [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)]
 36    public async Task<ActionResult<DataOutput<IssueApplicationCredentialCommandOutput?>>> Issue(
 37        Guid scopeId, Guid id,
 38        [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] IssueApplicationCredentialCommand? command)
 39    {
 40        // AF-53c — a body that is not JSON, or an expiresAt that is not a date-time, never reaches here:
 41        // MalformedRequestFilter answers it (NFR-10). Only a genuinely empty body means "no expiry".
 2042        command ??= new IssueApplicationCredentialCommand();
 2043        command.ScopeId = scopeId;
 2044        command.Id = id;
 2045        HttpContext.ApplyActor(command);
 46
 2047        var result = await commandMediator
 2048            .ExecuteCommandAsync<IssueApplicationCredentialCommand, IssueApplicationCredentialCommandOutput>(command);
 49
 2050        return result.ToActionResult(statusMap: ApplicationCredentialMessageMap.StatusCodes);
 2051    }
 52
 53    /// <summary>
 54    ///     Lists the application's credentials, newest first, with whether each is live (UC-54).
 55    ///     Never a secret or a hash.
 56    /// </summary>
 57    [HttpGet]
 58    [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)]
 59    public async Task<ActionResult<DataOutput<ApplicationCredentialListOutput?>>> List(Guid scopeId, Guid id)
 60    {
 661        var query = new ListApplicationCredentialsQuery { ScopeId = scopeId, Id = id };
 662        HttpContext.ApplyActor(query);
 63
 664        var result = await queryMediator
 665            .ExecuteQueryAsync<ListApplicationCredentialsQuery, ApplicationCredentialListOutput>(query);
 66
 667        return result.ToActionResult(statusMap: ApplicationCredentialMessageMap.StatusCodes);
 668    }
 69
 70    /// <summary>
 71    ///     Revokes one credential (UC-55). Its secret stops being exchanged at once, and every token
 72    ///     already issued from it fails the next live re-check (UC-57).
 73    /// </summary>
 74    [HttpDelete("{credentialId:guid}")]
 75    [RoleRequirement((int)Roles.SystemAdmin, (int)Roles.ScopeAdmin)]
 76    public async Task<ActionResult<DataOutput<RevokeApplicationCredentialCommandOutput?>>> Revoke(
 77        Guid scopeId, Guid id, Guid credentialId)
 78    {
 979        var command = new RevokeApplicationCredentialCommand { ScopeId = scopeId, Id = id, CredentialId = credentialId }
 980        HttpContext.ApplyActor(command);
 81
 982        var result = await commandMediator
 983            .ExecuteCommandAsync<RevokeApplicationCredentialCommand, RevokeApplicationCredentialCommandOutput>(command);
 84
 985        return result.ToActionResult(statusMap: ApplicationCredentialMessageMap.StatusCodes);
 986    }
 87}