< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.SetGoogleSignInCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/SetGoogleSignInCommandHandler.cs
Line coverage
97%
Covered lines: 38
Uncovered lines: 1
Coverable lines: 39
Total lines: 97
Line coverage: 97.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%8896.96%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Handlers/SetGoogleSignInCommandHandler.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.Shared.Messages;
 6using ArturRios.Heimdall.Shared.Services;
 7using ArturRios.Mediator.Command.Interfaces;
 8using ArturRios.Output;
 9using FluentValidation;
 10using Microsoft.EntityFrameworkCore;
 11
 12namespace ArturRios.Heimdall.Command.Handlers;
 13
 14/// <summary>
 15///     Handles <see cref="SetGoogleSignInCommand" /> (UC-24, FR-GO-01/FR-GO-02): validates that the
 16///     request said which value to set (AF-24c), verifies the target scope exists and is active (AF-24a),
 17///     enforces scope ownership for a Scope Admin actor (AF-24b), then writes the scope's
 18///     <c>GoogleSignInEnabled</c> flag and returns the updated scope. A System Admin actor bypasses
 19///     the ownership check. All failures are returned as errors on the <see cref="DataOutput{T}" />
 20///     rather than thrown, using the canonical <see cref="ScopeMessages" /> so the response resolver
 21///     can pick the matching status code.
 22/// </summary>
 1923public class SetGoogleSignInCommandHandler(
 1924    IValidator<SetGoogleSignInCommand> validator,
 1925    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 1926    IAsyncRepository<Scope, long> scopeWriter,
 1927    IScopeOwnershipChecker scopeOwnership)
 28    : ICommandHandlerAsync<SetGoogleSignInCommand, SetGoogleSignInCommandOutput>
 29{
 30    public async Task<DataOutput<SetGoogleSignInCommandOutput?>> HandleAsync(SetGoogleSignInCommand command, Cancellatio
 31    {
 1932        var output = DataOutput<SetGoogleSignInCommandOutput?>.New;
 33
 34        // AF-24c (UC-24 step 2, NFR-10): the request must state which value to set. Enabled is
 35        // nullable precisely so this check is possible — a plain bool would bind a body omitting the
 36        // field to false, turning a malformed request into the destructive half of the toggle.
 1937        var validation = await validator.ValidateAsync(command);
 38
 1939        if (!validation.IsValid)
 40        {
 441            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 42        }
 43
 44        // AF-24a (UC-24 step 3): the target scope must exist and not be logically deleted — the
 45        // alternative flow names both conditions as one outcome, so the filter answers for both.
 1746        var scope = await scopeReader.Query()
 1747            .FirstOrDefaultAsync(x => x.PublicId == command.Id && !x.IsDeleted);
 48
 1749        if (scope is null)
 50        {
 451            return output.WithError(ScopeMessages.ScopeNotFound);
 52        }
 53
 54        // AF-24b (UC-24 step 4): a Scope Admin actor may only act on a scope they own; a System Admin
 55        // bypasses. Checked before anything is written, so a refused caller leaves no trace.
 1356        if (!await scopeOwnership.ActorMayManageScopeAsync(command.ActingRole, command.ActingPersonId, scope.Id))
 57        {
 358            return output.WithError(ScopeMessages.NotScopeOwner);
 59        }
 60
 61        // Owner PublicIds for the response. A projection over the navigation (no Include needed),
 62        // which EF translates to a join and the in-memory fake evaluates directly.
 1063        var ownerIds = await scopeReader.Query()
 1064            .Where(x => x.PublicId == command.Id)
 1065            .SelectMany(x => x.Owners.Select(owner => owner.Person.PublicId))
 1066            .ToListAsync();
 67
 68        // UC-24 step 5: write the flag and stamp UpdatedAt (no DB trigger maintains it). Written
 69        // unconditionally: PUT is idempotent by contract and UC-24 defines no alternative flow for a
 70        // request that asks for the value the scope already holds.
 1071        scope.GoogleSignInEnabled = command.Enabled!.Value;
 1072        scope.UpdatedAt = DateTime.UtcNow;
 73
 1074        var update = await scopeWriter.UpdateAsync(scope);
 75
 1076        if (!update.Success)
 77        {
 078            return output.WithErrors(update.Errors);
 79        }
 80
 81        // UC-24 step 6: return the updated scope.
 1082        return output
 1083            .WithData(new SetGoogleSignInCommandOutput
 1084            {
 1085                Id = scope.PublicId,
 1086                Name = scope.Name,
 1087                Description = scope.Description,
 1088                GoogleSignInEnabled = scope.GoogleSignInEnabled,
 1089                EmailVerificationUrl = scope.EmailVerificationUrl,
 1090                PasswordResetUrl = scope.PasswordResetUrl,
 1091                OwnerIds = ownerIds,
 1092                CreatedAt = scope.CreatedAt,
 1093                UpdatedAt = scope.UpdatedAt
 1094            })
 1095            .WithMessage(ScopeMessages.GoogleSignInUpdatedSuccessfully);
 1996    }
 97}