< Summary - Heimdall API

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

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Query/Handlers/GetApplicationByIdQueryHandler.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Enums;
 3using ArturRios.Heimdall.Query.Input;
 4using ArturRios.Heimdall.Query.Output;
 5using ArturRios.Heimdall.Shared.Messages;
 6using ArturRios.Mediator.Query.Interfaces;
 7using ArturRios.Output;
 8using Microsoft.EntityFrameworkCore;
 9using Application = ArturRios.Heimdall.Domain.Entities.Application;
 10
 11namespace ArturRios.Heimdall.Query.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="GetApplicationByIdQuery" /> (UC-17, FR-AP-04): retrieves an application by
 15///     its <c>PublicId</c> within the addressed scope, excluding logically deleted applications
 16///     unless explicitly requested (FR-AP-09), then applies the use case's visibility rule — a System
 17///     Admin sees any application, a Scope Admin only the ones they own. A miss is AF-17a
 18///     (<c>ApplicationNotFound</c>); an application the caller may not see is AF-17b
 19///     (<c>NotAuthorizedToViewApplication</c>). Both are returned as errors rather than thrown.
 20/// </summary>
 1821public class GetApplicationByIdQueryHandler(IAsyncReadOnlyRepository<Application, long> applicationReader)
 22    : IQueryHandlerAsync<GetApplicationByIdQuery, ApplicationOutput>
 23{
 24    public async Task<DataOutput<ApplicationOutput?>> HandleAsync(GetApplicationByIdQuery query, CancellationToken cance
 25    {
 1826        var output = DataOutput<ApplicationOutput?>.New;
 27
 28        // The route's scopeId qualifies the lookup: an application that exists in another scope is
 29        // not the resource this path addresses, so it falls out here rather than at the rule below.
 1830        var found = await applicationReader.Query()
 1831            .Where(x => x.PublicId == query.Id && x.Scope.PublicId == query.ScopeId &&
 1832                        (query.IncludeDeleted || !x.IsDeleted))
 1833            .Select(x => new
 1834            {
 1835                Output = new ApplicationOutput
 1836                {
 1837                    Id = x.PublicId,
 1838                    Name = x.Name,
 1839                    ScopeId = x.Scope.PublicId,
 1840                    OwnerId = x.Owner.PublicId,
 1841                    IsDeleted = x.IsDeleted,
 1842                    CreatedAt = x.CreatedAt,
 1843                    UpdatedAt = x.UpdatedAt
 1844                },
 1845                OwnerOwnsScope = x.Owner.ScopeOwnerships.Any(ownership => ownership.ScopeId == x.ScopeId)
 1846            })
 1847            .FirstOrDefaultAsync();
 48
 1849        var application = found?.Output;
 50
 51        // AF-17a: no such application under this scope (or it is logically deleted and was not
 52        // explicitly requested). Checked before authorization, so both alternative flows stay
 53        // observable — a GUID nobody holds cannot be told apart from one the caller may not see.
 1854        if (application is null)
 55        {
 756            return output.WithError(ApplicationMessages.ApplicationNotFound);
 57        }
 58
 59        // AF-17b (UC-17 step 2): a System Admin sees every application; anyone else must own it.
 60        // Owning the *scope* is not by itself grounds to read another owner's application, so the
 61        // rule compares the owner rather than consulting IScopeOwnershipChecker.
 62        //
 63        // Owning the application is not enough by itself either: FR-AP-03 makes an owner someone who
 64        // owns the application's scope, and UC-22 can take that away while leaving them named as the
 65        // owner. Without this a Scope Admin removed from a scope kept reading, renaming and deleting
 66        // its applications — everything but listing them, which does check the scope.
 1167        if (query.ActingRole != (int)Roles.SystemAdmin &&
 1168            (application.OwnerId != query.ActingPersonId || !found!.OwnerOwnsScope))
 69        {
 470            return output.WithError(ApplicationMessages.NotAuthorizedToViewApplication);
 71        }
 72
 773        return output
 774            .WithData(application)
 775            .WithMessage(ApplicationMessages.ApplicationRetrievedSuccessfully);
 1876    }
 77}