< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Query.Handlers.GetScopeByIdQueryHandler
Assembly: ArturRios.Heimdall.Query
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Query/Handlers/GetScopeByIdQueryHandler.cs
Line coverage
100%
Covered lines: 42
Uncovered lines: 0
Coverable lines: 42
Total lines: 92
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage
100%
Covered methods: 5
Fully covered methods: 5
Total methods: 5
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_ActorBelongsToScope()100%11100%
get_Output()100%11100%
HandleAsync()100%44100%
MayView(...)100%44100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Enums;
 4using ArturRios.Heimdall.Query.Input;
 5using ArturRios.Heimdall.Query.Output;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Mediator.Query.Interfaces;
 8using ArturRios.Output;
 9using Microsoft.EntityFrameworkCore;
 10
 11namespace ArturRios.Heimdall.Query.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="GetScopeByIdQuery" /> (UC-02, FR-SC-02): retrieves a scope by its
 15///     <c>PublicId</c>, excluding logically deleted scopes unless explicitly requested (FR-SC-07),
 16///     then applies the use case's per-actor visibility rule. A missing scope is AF-02a
 17///     (<c>ScopeNotFound</c>); a scope the caller may not see is AF-02b
 18///     (<c>NotAuthorizedToViewScope</c>). Both are returned as errors rather than thrown.
 19/// </summary>
 1820public class GetScopeByIdQueryHandler(IAsyncReadOnlyRepository<Scope, long> scopeReader)
 21    : IQueryHandlerAsync<GetScopeByIdQuery, ScopeOutput>
 22{
 23    /// <summary>
 24    ///     The scope plus the one membership fact the visibility rule needs and
 25    ///     <see cref="ScopeOutput" /> does not carry. Ownership needs no extra field: the output
 26    ///     already lists the owners' <c>PublicId</c>s.
 27    /// </summary>
 28    private sealed class ScopeProjection
 29    {
 1930        public bool ActorBelongsToScope { get; init; }
 31
 2632        public ScopeOutput Output { get; init; } = null!;
 33    }
 34
 35    public async Task<DataOutput<ScopeOutput?>> HandleAsync(GetScopeByIdQuery query, CancellationToken cancellationToken
 36    {
 1837        var output = DataOutput<ScopeOutput?>.New;
 38
 1839        var scope = await scopeReader.Query()
 1840            .Where(x => x.PublicId == query.Id && (query.IncludeDeleted || !x.IsDeleted))
 1841            .Select(x => new ScopeProjection
 1842            {
 1843                ActorBelongsToScope = x.Users.Any(user => user.Person.PublicId == query.ActingPersonId),
 1844                Output = new ScopeOutput
 1845                {
 1846                    Id = x.PublicId,
 1847                    Name = x.Name,
 1848                    Description = x.Description,
 1849                    GoogleSignInEnabled = x.GoogleSignInEnabled,
 1850                    SelfRegistrationEnabled = x.SelfRegistrationEnabled,
 1851                    DefaultLegalBasis = x.DefaultLegalBasis,
 1852                    PrivacyNoticeUri = x.PrivacyNoticeUri,
 1853                    EmailVerificationUrl = x.EmailVerificationUrl,
 1854                    PasswordResetUrl = x.PasswordResetUrl,
 1855                    IsDeleted = x.IsDeleted,
 1856                    OwnerIds = x.Owners.Select(owner => owner.Person.PublicId).ToList(),
 1857                    CreatedAt = x.CreatedAt,
 1858                    UpdatedAt = x.UpdatedAt
 1859                }
 1860            })
 1861            .FirstOrDefaultAsync();
 62
 63        // AF-02a: no such scope (or it is logically deleted and was not explicitly requested).
 1864        if (scope is null)
 65        {
 466            return output.WithError(ScopeMessages.ScopeNotFound);
 67        }
 68
 69        // AF-02b: the caller is not allowed to see this scope.
 1470        if (!MayView(query, scope))
 71        {
 672            return output.WithError(ScopeMessages.NotAuthorizedToViewScope);
 73        }
 74
 875        return output
 876            .WithData(scope.Output)
 877            .WithMessage(ScopeMessages.ScopeRetrievedSuccessfully);
 1878    }
 79
 80    /// <summary>
 81    ///     UC-02 step 2: a System Admin sees every scope, a Scope Admin only the scopes they own, and
 82    ///     a User only the scope they belong to. Any other role is denied, so a role added later
 83    ///     cannot inherit read access to every scope by default.
 84    /// </summary>
 1485    private static bool MayView(GetScopeByIdQuery query, ScopeProjection scope) => query.ActingRole switch
 1486    {
 487        (int)Roles.SystemAdmin => true,
 488        (int)Roles.ScopeAdmin => scope.Output.OwnerIds.Contains(query.ActingPersonId),
 589        (int)Roles.User => scope.ActorBelongsToScope,
 190        _ => false
 1491    };
 92}