< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Query.Handlers.ListMyScopeInvitationsQueryHandler
Assembly: ArturRios.Heimdall.Query
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Query/Handlers/ListMyScopeInvitationsQueryHandler.cs
Line coverage
100%
Covered lines: 32
Uncovered lines: 0
Coverable lines: 32
Total lines: 69
Line coverage: 100%
Branch coverage
75%
Covered branches: 3
Total branches: 4
Branch coverage: 75%
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()75%44100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Query/Handlers/ListMyScopeInvitationsQueryHandler.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.Mediator.Query.Interfaces;
 6using ArturRios.Output;
 7using FluentValidation;
 8using Microsoft.EntityFrameworkCore;
 9
 10namespace ArturRios.Heimdall.Query.Handlers;
 11
 12/// <summary>
 13///     Handles <see cref="ListMyScopeInvitationsQuery" /> (UC-59, FR-SC-16): the caller's own open scope owner
 14///     invitations, soonest to expire first, for any signed-in Scope Admin or System Admin.
 15/// </summary>
 16/// <remarks>
 17///     Only open invitations, and not those of a logically deleted scope, which accepting would only void.
 18///     The inviter's name is shown only for a live, unrestricted inviter (NFR-24). The listing answers 200
 19///     with no message, and an empty page when there are none.
 20/// </remarks>
 721public class ListMyScopeInvitationsQueryHandler(
 722    IAsyncReadOnlyRepository<Person, long> personReader,
 723    IAsyncReadOnlyRepository<ScopeOwnerInvitation, long> invitationReader,
 724    IValidator<ListMyScopeInvitationsQuery> validator)
 25    : IPaginatedQueryHandlerAsync<ListMyScopeInvitationsQuery, MyScopeInvitationOutput>
 26{
 27    public async Task<PaginatedOutput<MyScopeInvitationOutput>> HandleAsync(
 28        ListMyScopeInvitationsQuery query, CancellationToken cancellationToken = default)
 29    {
 730        var output = PaginatedOutput<MyScopeInvitationOutput>.New;
 31
 32        // NFR-10: page bounds, before any query runs.
 733        var validation = await validator.ValidateAsync(query, cancellationToken);
 34
 735        if (!validation.IsValid)
 36        {
 237            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 38        }
 39
 40        // The liveness filter has already refused a caller whose record is gone; id 0 matches no row.
 641        var actor = await personReader.Query()
 642            .FirstOrDefaultAsync(x => x.PublicId == query.ActingPersonId, cancellationToken);
 643        var actorId = actor?.Id ?? 0;
 644        var now = DateTime.UtcNow;
 45
 46        // Soonest to expire first with the internal id as tiebreaker, ordered on the entity because the id
 47        // never reaches the output, then projected and paginated over that ordering.
 648        var projected = invitationReader.Query()
 649            .Where(x => x.InviteePersonId == actorId && !x.Scope.IsDeleted)
 650            .Where(ScopeOwnerInvitation.OpenAt(now))
 651            .OrderBy(x => x.ExpiresAt)
 652            .ThenBy(x => x.Id)
 653            .Select(x => new MyScopeInvitationOutput
 654            {
 655                Id = x.PublicId,
 656                ScopeId = x.Scope.PublicId,
 657                ScopeName = x.Scope.Name,
 658                // NFR-24: a removed or restricted inviter's name is withheld, as every person listing does.
 659                InvitedByName =
 660                    x.InvitedBy != null && !x.InvitedBy.IsDeleted && x.InvitedBy.ProcessingRestrictedAt == null
 661                        ? x.InvitedBy.Name
 662                        : null,
 663                CreatedAt = x.CreatedAt,
 664                ExpiresAt = x.ExpiresAt
 665            });
 66
 667        return await projected.PaginateAsync(query.PageNumber, query.PageSize, orderBy: null, cancellationToken: cancell
 768    }
 69}