< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.DeleteGoogleUserCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/DeleteGoogleUserCommandHandler.cs
Line coverage
96%
Covered lines: 27
Uncovered lines: 1
Coverable lines: 28
Total lines: 107
Line coverage: 96.4%
Branch coverage
87%
Covered branches: 7
Total branches: 8
Branch coverage: 87.5%
Method coverage
100%
Covered methods: 2
Fully covered methods: 1
Total methods: 2
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
HandleAsync()87.5%8895.83%

File(s)

/home/runner/work/heimdall-api/heimdall-api/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.Shared.Messages;
 7using ArturRios.Heimdall.Shared.Services;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="DeleteGoogleUserCommand" /> (UC-28, FR-GO-15): locates the Google User
 16///     inside the addressed scope (AF-28a), enforces the scope-ownership rule (AF-28c), serves an
 17///     already-deleted record as an idempotent no-op (AF-28b), and otherwise sets
 18///     <c>IsDeleted = true</c> and stamps <c>UpdatedAt</c>. Nothing cascades — see the remarks. All
 19///     failures are returned as errors on the <see cref="DataOutput{T}" /> rather than thrown.
 20/// </summary>
 21/// <remarks>
 22///     <para>
 23///         The shape of <see cref="DeleteApplicationCommandHandler" />, differing in step 2 only.
 24///         UC-19 compares the application's <em>owner</em>, because owning a scope is not grounds to
 25///         delete another owner's application; a Google User has no owner, only a scope (FR-GO-06),
 26///         and UC-28 names "a System Admin, or an owner of the Google User's scope" — which is
 27///         exactly what <see cref="IScopeOwnershipChecker" /> decides.
 28///     </para>
 29///     <para>
 30///         Nothing cascades because there is nothing to cascade to. A Google User can own no
 31///         application — FR-AP-03 restricts that to a <c>ScopeAdmin</c> who owns the scope, and a
 32///         Google User is always <c>User</c>-equivalent (FR-GO-04) — and it holds no password reset
 33///         or email verification tokens, since authentication is delegated to Google.
 34///     </para>
 35///     <para>
 36///         The flag this sets is already honoured everywhere it matters: UC-25 refuses to
 37///         authenticate a deleted Google User (AF-25d), UC-26 refuses their sign-out (AF-26a), and
 38///         UC-27 hides them from default reads (FR-GO-17).
 39///     </para>
 40/// </remarks>
 1541public class DeleteGoogleUserCommandHandler(
 1542    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1543    IAsyncRepository<GoogleUser, long> googleUserWriter,
 1544    IScopeOwnershipChecker scopeOwnership)
 45    : ICommandHandlerAsync<DeleteGoogleUserCommand, DeleteGoogleUserCommandOutput>
 46{
 47    public async Task<DataOutput<DeleteGoogleUserCommandOutput?>> HandleAsync(DeleteGoogleUserCommand command, Cancellat
 48    {
 1549        var output = DataOutput<DeleteGoogleUserCommandOutput?>.New;
 50
 51        // AF-28a: the Google User must exist inside the addressed scope. The lookup deliberately omits
 52        // the !IsDeleted filter UC-27's default read applies, so an already-deleted record is found
 53        // and served idempotently by AF-28b below rather than reported as not found — without that,
 54        // AF-28b could never fire. The route's scopeId qualifies it: a Google User that belongs to
 55        // another scope is not the resource this path addresses.
 1556        var googleUser = await googleUserReader.Query()
 1557            .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId);
 58
 1559        if (googleUser is null)
 60        {
 461            return output.WithError(GoogleUserMessages.GoogleUserNotFound);
 62        }
 63
 64        // AF-28c (UC-28 step 2): a System Admin may delete any Google User; a Scope Admin only those
 65        // of the scopes they own — which is what the checker decides, along with its guard that a
 66        // logically deleted actor owns nothing. Runs before AF-28b so an already-deleted Google User
 67        // cannot be used to probe for records outside the caller's reach.
 1168        if (!await scopeOwnership.ActorMayManageScopeAsync(
 1169                command.ActingRole, command.ActingPersonId, googleUser.ScopeId))
 70        {
 371            return output.WithError(GoogleUserMessages.NotAuthorizedToDeleteGoogleUser);
 72        }
 73
 74        // AF-28b: already deleted — whether directly or by UC-04's cascade from its scope — so there
 75        // is nothing to write. UpdatedAt is left alone: the row already carries the requested state,
 76        // and re-stamping it would misreport when the deletion happened.
 877        if (googleUser.IsDeleted)
 78        {
 279            return output
 280                .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = true })
 281                .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully);
 82        }
 83
 84        // UC-28 step 3: flip the flag and stamp UpdatedAt (no DB trigger maintains it).
 85        // DeletedAt and DeletionKind are stamped for NFR-20, for the reasons given in
 86        // DeletePersonCommandHandler: the anonymisation window is measured from the deletion, and
 87        // UpdatedAt would restart it on any later write.
 688        var deletedAt = DateTime.UtcNow;
 89
 690        googleUser.IsDeleted = true;
 691        googleUser.DeletedAt = deletedAt;
 692        googleUser.DeletionKind = (int)DeletionKinds.Administrative;
 693        googleUser.UpdatedAt = deletedAt;
 94
 695        var update = await googleUserWriter.UpdateAsync(googleUser);
 96
 697        if (!update.Success)
 98        {
 099            return output.WithErrors(update.Errors);
 100        }
 101
 102        // UC-28 step 4.
 6103        return output
 6104            .WithData(new DeleteGoogleUserCommandOutput { Id = googleUser.PublicId, AlreadyDeleted = false })
 6105            .WithMessage(GoogleUserMessages.GoogleUserDeletedSuccessfully);
 15106    }
 107}