< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.LockedPerson
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Services/LockedPerson.cs
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 73
Line coverage: 100%
Branch coverage
75%
Covered branches: 3
Total branches: 4
Branch coverage: 75%
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_RoleId()100%11100%
get_IsLiveSystemAdmin()100%22100%
LockAsync()50%22100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Services/LockedPerson.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Enums;
 4using ArturRios.Heimdall.Domain.Persistence;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace ArturRios.Heimdall.Command.Services;
 8
 9/// <summary>
 10///     A person about to be taken out — deleted (UC-09, UC-10) or suspended for erasure (UC-42 and the
 11///     anonymisation pass's retry) — as they stand once the invariant locks are held: their role, whether they
 12///     are deleted, and the scopes they own. What <see cref="LastScopeOwnerGuard" /> and
 13///     <see cref="LastSystemAdminGuard" /> decide on.
 14/// </summary>
 15/// <remarks>
 16///     <para>
 17///         <b>Read under the locks, not before them</b> (#202, #203). A copy read before the transaction can
 18///         be stale by the time the guards ask: a scope the person owned may have been hard-deleted, so its row
 19///         is gone from the lock and the guard would refuse for a scope that no longer exists; or the person may
 20///         have been promoted to System Admin, and a role read before the lock would skip NFR-32's guard.
 21///     </para>
 22///     <para>
 23///         <b>The System Admin set is locked for every one of them.</b> Which persons are administrators is
 24///         only known under the lock, so the lock cannot be chosen by the role: a User or Scope Admin promoted
 25///         while being deleted would otherwise be deleted as an administrator with no lock held. These writers
 26///         are administrative and rare, so serialising them on one key costs nothing that matters.
 27///     </para>
 28///     <para>
 29///         <b>An ownership gained after the locks</b> — a scope created naming the person (UC-03), an owner
 30///         added (UC-21, UC-23) — is neither locked nor checked: the writers that add owners take no lock, since
 31///         adding one can never leave a scope ownerless on its own. The System Requirements record that
 32///         residual.
 33///     </para>
 34/// </remarks>
 35/// <param name="RoleId">The person's role under the locks.</param>
 36/// <param name="IsDeleted">Whether the person is logically deleted under the locks.</param>
 37/// <param name="OwnedScopeIds">The scopes the person owns under the locks.</param>
 27538public sealed record LockedPerson(long RoleId, bool IsDeleted, IReadOnlyCollection<long> OwnedScopeIds)
 39{
 40    /// <summary>Whether the person is a System Admin NFR-32 protects: one not yet deleted.</summary>
 6041    public bool IsLiveSystemAdmin => RoleId == (long)Roles.SystemAdmin && !IsDeleted;
 42
 43    /// <summary>
 44    ///     Locks the scopes the person <paramref name="personId" /> owns, then the System Admin set — in the one
 45    ///     <see cref="IInvariantLocks.LockAsync" /> call the lock order asks for, in the caller's transaction —
 46    ///     and reads the person again under them. <c>null</c> when the person is gone.
 47    /// </summary>
 48    public static async Task<LockedPerson?> LockAsync(
 49        IInvariantLocks locks, IAsyncReadOnlyRepository<Person, long> personReader, long personId)
 50    {
 8951        var scopeIds = await personReader.Query()
 8952            .Where(person => person.Id == personId)
 8953            .SelectMany(person => person.ScopeOwnerships.Select(ownership => ownership.ScopeId))
 8954            .ToListAsync();
 55
 8956        await locks.LockAsync(scopeIds, systemAdmins: true);
 57
 58        // A projection, so the values are the database's as they stand now rather than a tracked copy's.
 59        // The ownerships are those the person still holds: a scope deleted since the read above is gone
 60        // from them, as it is from the lock.
 8961        var current = await personReader.Query()
 8962            .Where(person => person.Id == personId)
 8963            .Select(person => new
 8964            {
 8965                person.RoleId,
 8966                person.IsDeleted,
 8967                OwnedScopeIds = person.ScopeOwnerships.Select(ownership => ownership.ScopeId).ToList()
 8968            })
 8969            .FirstOrDefaultAsync();
 70
 8971        return current is null ? null : new LockedPerson(current.RoleId, current.IsDeleted, current.OwnedScopeIds);
 8972    }
 73}