< Summary - Heimdall API

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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
WouldStripLastOwnerAsync()100%22100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using Microsoft.EntityFrameworkCore;
 4
 5namespace ArturRios.Heimdall.Command.Services;
 6
 7/// <summary>
 8///     NFR-12: whether taking a person out of the scopes they own — removing one ownership (UC-22),
 9///     deleting them (UC-09, UC-10), or suspending them for erasure (UC-42 and the anonymisation pass's
 10///     retry) — would leave one of those scopes with no active owner.
 11/// </summary>
 12/// <remarks>
 13///     <para>
 14///         <b>One guard for every writer.</b> UC-09, UC-10 and UC-22 used to carry their own copies; the
 15///         definition of who keeps a scope owned now lives in one place, <see cref="Person.Active" />
 16///         (#211).
 17///     </para>
 18///     <para>
 19///         <b>Who counts.</b> Only an active co-owner: not logically deleted, no erasure requested, and
 20///         processing not restricted. A suspended owner cannot authenticate, a restricted one is refused
 21///         on every action but the data-subject ones and hidden from the owner list, and one whose
 22///         erasure is requested is on the way out — none of them can administer the scope, so none
 23///         keeps it owned.
 24///     </para>
 25///     <para>
 26///         <b>Asked under the scope locks (#202).</b> On its own the answer is stale the moment it is read:
 27///         two requests taking out the two owners of one scope each read the other still there, both
 28///         write, and nobody is left. Every caller therefore asks inside its transaction, once
 29///         <c>IInvariantLocks</c> holds the scopes in question — <see cref="LockedPerson.LockAsync" />
 30///         for a person's — so a competing writer of the same scope waits until the answer has been acted
 31///         on, and then reads what was written.
 32///     </para>
 33/// </remarks>
 34public static class LastScopeOwnerGuard
 35{
 36    /// <summary>
 37    ///     Whether any of <paramref name="scopeIds" /> is owned by nobody active but the person
 38    ///     <paramref name="personId" />.
 39    /// </summary>
 40    public static async Task<bool> WouldStripLastOwnerAsync(
 41        long personId, IReadOnlyCollection<long> scopeIds, IAsyncReadOnlyRepository<Person, long> personReader)
 42    {
 43        // Whatever the role: a SystemAdmin may own a scope too (FR-SC-08).
 10944        if (scopeIds.Count == 0)
 45        {
 4846            return false;
 47        }
 48
 6149        var coOwnedScopeIds = await personReader.Query()
 6150            .Where(Person.Active)
 6151            .Where(other => other.Id != personId)
 6152            .SelectMany(other => other.ScopeOwnerships.Select(ownership => ownership.ScopeId))
 6153            .Where(scopeId => scopeIds.Contains(scopeId))
 6154            .Distinct()
 6155            .ToListAsync();
 56
 12457        return scopeIds.Any(scopeId => !coOwnedScopeIds.Contains(scopeId));
 10958    }
 59}

Methods/Properties

WouldStripLastOwnerAsync()