< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Auditing.AuditingCommandHandler<T1, T2>
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Auditing/AuditingCommandHandler.cs
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 78
Line coverage: 100%
Branch coverage
93%
Covered branches: 15
Total branches: 16
Branch coverage: 93.7%
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%
HandleAsync()100%88100%
TryWriteAsync()100%11100%
ResolveTargetId(...)87.5%88100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Auditing/AuditingCommandHandler.cs

#LineLine coverage
 1using ArturRios.Mediator.Command;
 2using ArturRios.Mediator.Command.Interfaces;
 3using ArturRios.Output;
 4using Microsoft.Extensions.Logging;
 5
 6namespace ArturRios.Heimdall.Command.Auditing;
 7
 8/// <summary>
 9///     Wraps a command handler so every attempted write produces one audit trail entry (NFR-09),
 10///     without changing the wrapped handler — plus one more for each side effect a successful
 11///     command's output reports through <see cref="IAuditedSideEffects" />. Registered per-handler by
 12///     <see cref="CommandHandlerRegistrationExtensions.AddAuditedCommandHandler{TCommand,TOutput,THandler}" />.
 13/// </summary>
 98814public class AuditingCommandHandler<TCommand, TOutput>(
 98815    ICommandHandlerAsync<TCommand, TOutput> inner,
 98816    IAuditLogWriter auditLogWriter,
 98817    ILogger<AuditingCommandHandler<TCommand, TOutput>> logger)
 18    : ICommandHandlerAsync<TCommand, TOutput>
 19    where TCommand : BaseCommand
 20    where TOutput : CommandOutput
 21{
 22    public async Task<DataOutput<TOutput?>> HandleAsync(TCommand command, CancellationToken cancellationToken = default)
 23    {
 98824        var result = await inner.HandleAsync(command, cancellationToken);
 25
 98626        var action = typeof(TCommand).Name;
 98627        var targetId = ResolveTargetId(result.Data);
 28
 29        // Refusals are recorded too, and they are usually the entries worth having: a caller
 30        // repeatedly denied a scope they do not own, or repeatedly failing a password, leaves no
 31        // other trace anywhere. Only the first error is kept — enough to say why, and all of them
 32        // are the application's own canonical messages, so nothing a caller submitted is written
 33        // here.
 98634        await TryWriteAsync(
 98635            action, targetId, result.Success, result.Success ? null : result.Errors.FirstOrDefault());
 36
 37        // A side effect only happened if the command did, so a refusal records none.
 98638        if (result.Success && result.Data is IAuditedSideEffects sideEffects)
 39        {
 3440            foreach (var sideEffect in sideEffects.AuditedSideEffects())
 41            {
 642                await TryWriteAsync(sideEffect, targetId, true, null);
 43            }
 44        }
 45
 98646        return result;
 98647    }
 48
 49    private async Task TryWriteAsync(string action, Guid? targetId, bool succeeded, string? failureReason)
 50    {
 51        try
 52        {
 99253            await auditLogWriter.WriteAsync(action, targetId, succeeded, failureReason);
 99054        }
 255        catch (Exception exception)
 56        {
 57            // The trail is a record of what the API did, not part of doing it: a caller whose
 58            // request succeeded is not told it failed because the entry could not be written.
 259            logger.LogWarning(
 260                exception, "Failed to write audit log entry for {Action} (target {TargetId})",
 261                action, targetId);
 262        }
 99263    }
 64
 65    private static Guid? ResolveTargetId(TOutput? output)
 66    {
 98667        if (output is null)
 68        {
 54169            return null;
 70        }
 71
 44572        var property = typeof(TOutput).GetProperty("Id") ?? typeof(TOutput).GetProperty("PublicId");
 73
 44574        return property is not null && property.PropertyType == typeof(Guid)
 44575            ? (Guid?)property.GetValue(output)
 44576            : null;
 77    }
 78}