< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.HardDeleteGoogleUserCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/HardDeleteGoogleUserCommandHandler.cs
Line coverage
96%
Covered lines: 24
Uncovered lines: 1
Coverable lines: 25
Total lines: 92
Line coverage: 96%
Branch coverage
75%
Covered branches: 3
Total branches: 4
Branch coverage: 75%
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()75%4495.23%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/HardDeleteGoogleUserCommandHandler.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.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="HardDeleteGoogleUserCommand" /> (UC-29, FR-GO-16): locates the Google User
 16///     inside the addressed scope in any deletion state (AF-29a) and permanently removes the record.
 17///     Nothing cascades — the Deletion Strategy says a hard delete here "simply removes its record",
 18///     and the scope its foreign key points at is left intact. Authorization is entirely the
 19///     endpoint's: UC-29's only actor is the System Admin, so no rule is left for the handler to
 20///     apply. All failures are returned as errors on the <see cref="DataOutput{T}" /> rather than
 21///     thrown.
 22/// </summary>
 23/// <remarks>
 24///     <para>
 25///         No self-deletion refusal, unlike <see cref="DeletePersonCommandHandler" /> (AF-09d) and
 26///         <see cref="HardDeletePersonCommandHandler" /> (AF-10c). A Google User is always
 27///         <c>User</c>-equivalent (FR-GO-04) and can never hold <c>SystemAdmin</c>, so the only actor
 28///         who may call this endpoint can never be its target — there is no way to lock yourself out,
 29///         and a guard against it would be a flow no document defines.
 30///     </para>
 31///     <para>
 32///         Before the record is removed, its <c>PublicId</c> is tombstoned
 33///         (<see cref="ErasedSubject" />, NFR-21, #195), so the audit entries that name it as their
 34///         target — this hard delete's own among them — can still be cleared once nothing else says the
 35///         id was a person's. Written first, a failed delete leaves a tombstone for a live row, which
 36///         nothing acts on; and a retry writes it again without error.
 37///     </para>
 38/// </remarks>
 1439public class HardDeleteGoogleUserCommandHandler(
 1440    IAsyncReadOnlyRepository<GoogleUser, long> googleUserReader,
 1441    IAsyncRepository<GoogleUser, long> googleUserWriter,
 1442    IErasedSubjects erasedSubjects)
 43    : ICommandHandlerAsync<HardDeleteGoogleUserCommand, HardDeleteGoogleUserCommandOutput>
 44{
 45    public async Task<DataOutput<HardDeleteGoogleUserCommandOutput?>> HandleAsync(
 46        HardDeleteGoogleUserCommand command, CancellationToken cancellationToken = default)
 47    {
 1548        var output = DataOutput<HardDeleteGoogleUserCommandOutput?>.New;
 49
 50        // AF-29a: the Google User must exist inside the addressed scope. The lookup omits an
 51        // !IsDeleted filter — a logically deleted Google User, whether by UC-28 or by UC-04's cascade
 52        // from its scope, is exactly what a cleanup pass starts from and must still be purgeable, the
 53        // same call UC-05, UC-10, and UC-20 make. The route's scopeId qualifies it: an unknown Google
 54        // User, an unknown scope, and a Google User living in another scope are all one 404, because
 55        // the addressed resource genuinely does not exist in any of the three cases. A repeated call
 56        // lands here too — the row is already gone, and UC-29 has no idempotent path as UC-28 does.
 1557        var googleUser = await googleUserReader.Query()
 1558            .FirstOrDefaultAsync(x => x.PublicId == command.Id && x.Scope.PublicId == command.ScopeId);
 59
 1560        if (googleUser is null)
 61        {
 662            return output.WithError(GoogleUserMessages.GoogleUserNotFound);
 63        }
 64
 65        // UC-29 step 2 (NFR-21, #195): the tombstone goes first, so the row is never gone without it.
 966        await erasedSubjects.RecordAsync(
 967        [
 968            new ErasedSubject
 969            {
 970                Id = googleUser.PublicId,
 971                Kind = (int)ErasedSubjectKinds.GoogleUser,
 972                CreatedAt = DateTime.UtcNow
 973            }
 974        ]);
 75
 76        // UC-29 step 3: remove the record for good. No dependent is deleted first — a Google User can
 77        // own no application (FR-AP-03) and holds no password reset or email verification tokens,
 78        // since authentication is delegated to Google — so no foreign key can be violated and there
 79        // is no total to report, unlike the scope and person hard deletes.
 980        var delete = await googleUserWriter.DeleteAsync(googleUser);
 81
 982        if (!delete.Success)
 83        {
 084            return output.WithErrors(delete.Errors);
 85        }
 86
 87        // UC-29 step 4.
 988        return output
 989            .WithData(new HardDeleteGoogleUserCommandOutput { Id = googleUser.PublicId })
 990            .WithMessage(GoogleUserMessages.GoogleUserHardDeletedSuccessfully);
 1591    }
 92}