< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.DeleteGoogleUserCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/DeleteGoogleUserCommandHandler.cs
Line coverage
100%
Covered lines: 24
Uncovered lines: 0
Coverable lines: 24
Total lines: 105
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
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%88100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Command.Input;
 3using ArturRios.Heimdall.Command.Output;
 4using ArturRios.Heimdall.Domain.Entities;
 5using ArturRios.Heimdall.Domain.Enums;
 6using ArturRios.Heimdall.Domain.Persistence;
 7using ArturRios.Heimdall.Shared.Messages;
 8using ArturRios.Heimdall.Shared.Services;
 9using ArturRios.Mediator.Command.Interfaces;
 10using ArturRios.Output;
 11using Microsoft.EntityFrameworkCore;
 12
 13namespace ArturRios.Heimdall.Command.Handlers;
 14
 15/// <summary>
 16///     Handles <see cref="DeleteGoogleUserCommand" /> (UC-28, FR-GO-15): locates the Google User
 17///     inside the addressed scope (AF-28a), enforces the scope-ownership rule (AF-28c), serves an
 18///     already-deleted record as an idempotent no-op (AF-28b), and otherwise sets
 19///     <c>IsDeleted = true</c> and stamps <c>UpdatedAt</c>. Nothing cascades — see the remarks. All
 20///     failures are returned as errors on the <see cref="DataOutput{T}" /> rather than thrown.
 21/// </summary>
 22/// <remarks>
 23///     <para>
 24///         The shape of <see cref="DeleteApplicationCommandHandler" />, differing in step 2 only.
 25///         UC-19 compares the application's <em>owner</em>, because owning a scope is not grounds to
 26///         delete another owner's application; a Google User has no owner, only a scope (FR-GO-06),
 27///         and UC-28 names "a System Admin, or an owner of the Google User's scope" — which is
 28///         exactly what <see cref="IScopeOwnershipChecker" /> decides.
 29///     </para>
 30///     <para>
 31///         Nothing cascades because there is nothing to cascade to. A Google User can own no
 32///         application — FR-AP-03 restricts that to a <c>ScopeAdmin</c> who owns the scope, and a
 33///         Google User is always <c>User</c>-equivalent (FR-GO-04) — and it holds no password reset
 34///         or email verification tokens, since authentication is delegated to Google.
 35///     </para>
 36///     <para>
 37///         The flag this sets is already honoured everywhere it matters: UC-25 refuses to
 38///         authenticate a deleted Google User (AF-25d), UC-26 refuses their sign-out (AF-26a), and
 39///         UC-27 hides them from default reads (FR-GO-17).
 40///     </para>
 41/// </remarks>
 1842public class DeleteGoogleUserCommandHandler(
 1843    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1844    IAtomicWrites atomicWrites,
 1845    IScopeOwnershipChecker scopeOwnership)
 46    : ICommandHandlerAsync<DeleteGoogleUserCommand, DeleteGoogleUserCommandOutput>
 47{
 48    public async Task<DataOutput<DeleteGoogleUserCommandOutput?>> HandleAsync(DeleteGoogleUserCommand command, Cancellat
 49    {
 1850        var output = DataOutput<DeleteGoogleUserCommandOutput?>.New;
 51
 52        // AF-28a: the Google User must exist inside the addressed scope. The lookup deliberately omits
 53        // the !IsDeleted filter UC-27's default read applies, so an already-deleted record is found
 54        // and served idempotently by AF-28b below rather than reported as not found — without that,
 55        // AF-28b could never fire. The route's scopeId qualifies it: a Google User that belongs to
 56        // another scope is not the resource this path addresses.
 1857        var googleUser = await googleUserReader.Query()
 1858            .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId);
 59
 1860        if (googleUser is null)
 61        {
 462            return output.WithError(GoogleUserMessages.GoogleUserNotFound);
 63        }
 64
 65        // AF-28c (UC-28 step 2): a System Admin may delete any Google User; a Scope Admin only those
 66        // of the scopes they own — which is what the checker decides, along with its guard that a
 67        // logically deleted actor owns nothing. Runs before AF-28b so an already-deleted Google User
 68        // cannot be used to probe for records outside the caller's reach.
 1469        if (!await scopeOwnership.ActorMayManageScopeAsync(
 1470                command.ActingRole, command.ActingPersonId, googleUser.ScopeId))
 71        {
 372            return output.WithError(GoogleUserMessages.NotAuthorizedToDeleteGoogleUser);
 73        }
 74
 75        // AF-28b: already deleted — whether directly or by UC-04's cascade from its scope — so there
 76        // is nothing to write. UpdatedAt is left alone: the row already carries the requested state,
 77        // and re-stamping it would misreport when the deletion happened.
 1178        if (googleUser.IsDeleted)
 79        {
 280            return output
 281                .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = true })
 282                .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully);
 83        }
 84
 85        // UC-28 step 3: flip the flag and stamp UpdatedAt (no DB trigger maintains it).
 86        // DeletedAt and DeletionKind are stamped for NFR-20, for the reasons given in
 87        // DeletePersonCommandHandler: the anonymisation window is measured from the deletion, and
 88        // UpdatedAt would restart it on any later write.
 89        //
 90        // A targeted write of those four columns alone (#205), and only while the Google User is live:
 91        // a restriction made since the read stands beside the deletion, and a deletion made since — the
 92        // subject's own erasure request, say — stands as it was, answered as AF-28b.
 993        if (!await atomicWrites.TryMarkDeletedAsync(googleUser, DeletionKinds.Administrative, DateTime.UtcNow))
 94        {
 295            return output
 296                .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = true })
 297                .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully);
 98        }
 99
 100        // UC-28 step 4.
 7101        return output
 7102            .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = false })
 7103            .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully);
 18104    }
 105}