< Summary - Heimdall API

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

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Query/Handlers/ListSystemAdminsQueryHandler.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 FluentValidation;
 10
 11namespace ArturRios.Heimdall.Query.Handlers;
 12
 13/// <summary>
 14///     Handles <see cref="ListSystemAdminsQuery" /> (UC-07 read e, FR-PE-13): lists every live
 15///     <c>SystemAdmin</c> with pagination and optional name/email filters, projected to identifier,
 16///     name, and email only — who else holds full access to the deployment.
 17/// </summary>
 18/// <remarks>
 19///     Authorization is entirely the endpoint's, as in <see cref="ListErasureRequestsQueryHandler" />:
 20///     the only actor is the System Admin, and the <c>RoleRequirement</c> settles it, so there is no
 21///     data-dependent rule left here. A Scope Admin is refused because a System Admin's address is
 22///     not tenant-facing — the same reason <see cref="ListScopeAdminsQueryHandler" /> leaves System
 23///     Admins out of a Scope Admin's owner picker.
 24/// </remarks>
 1925public class ListSystemAdminsQueryHandler(
 1926    IAsyncReadOnlyRepository<Person, long> personReader,
 1927    IValidator<ListSystemAdminsQuery> validator)
 28    : IPaginatedQueryHandlerAsync<ListSystemAdminsQuery, PersonSummaryOutput>
 29{
 30    public async Task<PaginatedOutput<PersonSummaryOutput>> HandleAsync(ListSystemAdminsQuery query, CancellationToken c
 31    {
 1932        var output = PaginatedOutput<PersonSummaryOutput>.New;
 33
 34        // NFR-10: page number/size bounds and filter length, validated before any query runs.
 1935        var validation = await validator.ValidateAsync(query);
 36
 1937        if (!validation.IsValid)
 38        {
 1439            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 40        }
 41
 42        // A logically deleted administrator holds no access, so this listing has no include-deleted
 43        // mode at all — see ListSystemAdminsQuery.
 1244        var admins = personReader.Query()
 1245            .Where(x => x.RoleId == (long)Roles.SystemAdmin && !x.IsDeleted
 1246                        // NFR-24: withheld from listings, as in ListScopeAdmins. A System Admin can
 1247                        // still reach a restricted one by identifier (UC-07 read a).
 1248                        && x.ProcessingRestrictedAt == null);
 49
 1250        if (!string.IsNullOrWhiteSpace(query.Name))
 51        {
 352            var name = query.Name.ToLower();
 353            admins = admins.Where(x => x.Name.ToLower().Contains(name));
 54        }
 55
 1256        if (!string.IsNullOrWhiteSpace(query.Email))
 57        {
 258            var email = query.Email.ToLower();
 259            admins = admins.Where(x => x.Email.ToLower().Contains(email));
 60        }
 61
 1262        var projected = admins.Select(x => new PersonSummaryOutput
 1263        {
 1264            Id = x.PublicId,
 1265            Name = x.Name,
 1266            Email = x.Email
 1267        });
 68
 69        // Ordered by name with the public identifier as a tiebreaker, then paginated over that
 70        // ordering — the same reasoning ListScopePersonsQueryHandler documents: names are not
 71        // unique, PostgreSQL gives no ordering guarantee between tied sort keys, and each page is a
 72        // separate query, so without the tiebreaker two administrators sharing a name could straddle
 73        // a page boundary and appear on both pages while a third appeared on neither.
 1274        var ordered = projected.OrderBy(x => x.Name).ThenBy(x => x.Id);
 75
 1276        var page = await ordered.PaginateAsync(query.PageNumber, query.PageSize, orderBy: null);
 77
 1278        return page.WithMessage(PersonMessages.PersonsRetrievedSuccessfully);
 1979    }
 80}