< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Query.Handlers.GetGoogleUserByIdQueryHandler
Assembly: ArturRios.Heimdall.Query
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Query/Handlers/GetGoogleUserByIdQueryHandler.cs
Line coverage
100%
Covered lines: 37
Uncovered lines: 0
Coverable lines: 37
Total lines: 91
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
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%
get_ScopeInternalId()100%11100%
get_Output()100%11100%
HandleAsync()100%66100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Query.Input;
 4using ArturRios.Heimdall.Query.Output;
 5using ArturRios.Heimdall.Shared.Messages;
 6using ArturRios.Heimdall.Shared.Services;
 7using ArturRios.Mediator.Query.Interfaces;
 8using ArturRios.Output;
 9using Microsoft.EntityFrameworkCore;
 10
 11namespace ArturRios.Heimdall.Query.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="GetGoogleUserByIdQuery" /> (UC-27, FR-GO-14): retrieves a Google User by its
 15///     <c>PublicId</c> within the addressed scope, excluding logically deleted records unless
 16///     explicitly requested (FR-GO-17), then applies the use case's visibility rule. A miss is AF-27a
 17///     (<c>GoogleUserNotFound</c>); a record the caller may not see is AF-27b
 18///     (<c>NotAuthorizedToViewGoogleUser</c>). Both are returned as errors rather than thrown.
 19/// </summary>
 1920public class GetGoogleUserByIdQueryHandler(
 1921    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1922    IScopeOwnershipChecker scopeOwnership)
 23    : IQueryHandlerAsync<GetGoogleUserByIdQuery, GoogleUserOutput>
 24{
 25    /// <summary>
 26    ///     The Google User's payload plus the internal scope id the ownership rule needs. Internal ids
 27    ///     never reach the caller — only <see cref="Output" /> is returned (NFR-15).
 28    /// </summary>
 29    private sealed class GoogleUserProjection
 30    {
 2231        public long ScopeInternalId { get; init; }
 32
 3233        public GoogleUserOutput Output { get; init; } = null!;
 34    }
 35
 36    public async Task<DataOutput<GoogleUserOutput?>> HandleAsync(GetGoogleUserByIdQuery query, CancellationToken cancell
 37    {
 1938        var output = DataOutput<GoogleUserOutput?>.New;
 39
 40        // The route's scopeId qualifies the lookup: a Google User that exists in another scope is not
 41        // the resource this path addresses, so it falls out here rather than at the rule below. The
 42        // same arrangement GetApplicationByIdQueryHandler uses.
 1943        var googleUser = await googleUserReader.Query()
 1944            .Where(x => x.PublicId == query.Id && x.Scope.PublicId == query.ScopeId &&
 1945                        (query.IncludeDeleted || !x.IsDeleted))
 1946            .Select(x => new GoogleUserProjection
 1947            {
 1948                ScopeInternalId = x.ScopeId,
 1949                Output = new GoogleUserOutput
 1950                {
 1951                    Id = x.PublicId,
 1952                    GoogleId = x.GoogleId,
 1953                    Name = x.Name,
 1954                    Email = x.Email,
 1955                    EmailVerified = x.EmailVerified,
 1956                    ProfilePictureUrl = x.ProfilePictureUrl,
 1957                    IsDeleted = x.IsDeleted,
 1958                    ScopeId = x.Scope.PublicId,
 1959                    CreatedAt = x.CreatedAt,
 1960                    UpdatedAt = x.UpdatedAt
 1961                }
 1962            })
 1963            .FirstOrDefaultAsync();
 64
 65        // AF-27a: no such Google User under this scope (or it is logically deleted and was not
 66        // explicitly requested). Checked before authorization, so both alternative flows stay
 67        // observable — a GUID nobody holds cannot be told apart from one the caller may not see.
 1968        if (googleUser is null)
 69        {
 770            return output.WithError(GoogleUserMessages.GoogleUserNotFound);
 71        }
 72
 73        // AF-27b (UC-27 step 2). The use case grants three actors: a Google User reading themselves,
 74        // a System Admin reading anyone, and a Scope Admin reading the scopes they own. The last two
 75        // are exactly what IScopeOwnershipChecker decides, so the rule is the self-read plus that
 76        // check — which also inherits its guard that a logically deleted actor owns nothing. A
 77        // password User matches neither half, which is correct: the authorization matrix grants a
 78        // User this read only as self, and a person is never a Google User's self.
 1279        if (query.ActingPersonId != googleUser.Output.Id &&
 1280            !await scopeOwnership.ActorMayManageScopeAsync(
 1281                query.ActingRole, query.ActingPersonId, googleUser.ScopeInternalId))
 82        {
 483            return output.WithError(GoogleUserMessages.NotAuthorizedToViewGoogleUser);
 84        }
 85
 86        // UC-27 step 4.
 887        return output
 888            .WithData(googleUser.Output)
 889            .WithMessage(GoogleUserMessages.GoogleUserRetrievedSuccessfully);
 1990    }
 91}