< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.SetGoogleSignInCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/SetGoogleSignInCommandHandler.cs
Line coverage
100%
Covered lines: 36
Uncovered lines: 0
Coverable lines: 36
Total lines: 96
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/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.Domain.Persistence;
 6using ArturRios.Heimdall.Shared.Messages;
 7using ArturRios.Heimdall.Shared.Services;
 8using ArturRios.Mediator.Command.Interfaces;
 9using ArturRios.Output;
 10using FluentValidation;
 11using Microsoft.EntityFrameworkCore;
 12
 13namespace ArturRios.Heimdall.Command.Handlers;
 14
 15/// <summary>
 16///     Handles <see cref="SetGoogleSignInCommand" /> (UC-24, FR-GO-01/FR-GO-02): validates that the
 17///     request said which value to set (AF-24c), verifies the target scope exists and is active (AF-24a),
 18///     enforces scope ownership for a Scope Admin actor (AF-24b), then writes the scope's
 19///     <c>GoogleSignInEnabled</c> flag and returns the updated scope. A System Admin actor bypasses
 20///     the ownership check. All failures are returned as errors on the <see cref="DataOutput{T}" />
 21///     rather than thrown, using the canonical <see cref="ScopeMessages" /> so the response resolver
 22///     can pick the matching status code.
 23/// </summary>
 2124public class SetGoogleSignInCommandHandler(
 2125    IValidator<SetGoogleSignInCommand> validator,
 2126    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 2127    IAtomicWrites atomicWrites,
 2128    IScopeOwnershipChecker scopeOwnership)
 29    : ICommandHandlerAsync<SetGoogleSignInCommand, SetGoogleSignInCommandOutput>
 30{
 31    public async Task<DataOutput<SetGoogleSignInCommandOutput?>> HandleAsync(SetGoogleSignInCommand command, Cancellatio
 32    {
 2133        var output = DataOutput<SetGoogleSignInCommandOutput?>.New;
 34
 35        // AF-24c (UC-24 step 2, NFR-10): the request must state which value to set. Enabled is
 36        // nullable precisely so this check is possible — a plain bool would bind a body omitting the
 37        // field to false, turning a malformed request into the destructive half of the toggle.
 2138        var validation = await validator.ValidateAsync(command);
 39
 2140        if (!validation.IsValid)
 41        {
 442            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 43        }
 44
 45        // AF-24a (UC-24 step 3): the target scope must exist and not be logically deleted — the
 46        // alternative flow names both conditions as one outcome, so the filter answers for both.
 1947        var scope = await scopeReader.Query()
 1948            .FirstOrDefaultAsync(x => x.PublicId == command.Id && !x.IsDeleted);
 49
 1950        if (scope is null)
 51        {
 452            return output.WithError(ScopeMessages.ScopeNotFound);
 53        }
 54
 55        // AF-24b (UC-24 step 4): a Scope Admin actor may only act on a scope they own; a System Admin
 56        // bypasses. Checked before anything is written, so a refused caller leaves no trace.
 1557        if (!await scopeOwnership.ActorMayManageScopeAsync(command.ActingRole, command.ActingPersonId, scope.Id))
 58        {
 359            return output.WithError(ScopeMessages.NotScopeOwner);
 60        }
 61
 62        // Owner PublicIds for the response. A projection over the navigation (no Include needed),
 63        // which EF translates to a join and the in-memory fake evaluates directly.
 1264        var ownerIds = await scopeReader.Query()
 1265            .Where(x => x.PublicId == command.Id)
 1266            .SelectMany(x => x.Owners.Select(owner => owner.Person.PublicId))
 1267            .ToListAsync();
 68
 69        // UC-24 step 5: write the flag and stamp UpdatedAt (no DB trigger maintains it). Written
 70        // unconditionally: PUT is idempotent by contract and UC-24 defines no alternative flow for a
 71        // request that asks for the value the scope already holds. The flag alone, and only while the
 72        // scope is not deleted (#205): a save of the copy read above would write the scope's other
 73        // columns back as read — its name, the other settings, and the deletion flag, reviving a scope
 74        // deleted since. One deleted since is answered as AF-24a.
 1275        if (!await atomicWrites.TrySetGoogleSignInAsync(scope, command.Enabled!.Value, DateTime.UtcNow))
 76        {
 177            return output.WithError(ScopeMessages.ScopeNotFound);
 78        }
 79
 80        // UC-24 step 6: return the updated scope.
 1181        return output
 1182            .WithData(new SetGoogleSignInCommandOutput
 1183            {
 1184                Id = scope.PublicId,
 1185                Name = scope.Name,
 1186                Description = scope.Description,
 1187                GoogleSignInEnabled = scope.GoogleSignInEnabled,
 1188                EmailVerificationUrl = scope.EmailVerificationUrl,
 1189                PasswordResetUrl = scope.PasswordResetUrl,
 1190                OwnerIds = ownerIds,
 1191                CreatedAt = scope.CreatedAt,
 1192                UpdatedAt = scope.UpdatedAt
 1193            })
 1194            .WithMessage(ScopeMessages.GoogleSignInUpdatedSuccessfully);
 2195    }
 96}