< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Handlers.SetScopeEmailLinksCommandHandler
Assembly: ArturRios.Heimdall.Command
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/SetScopeEmailLinksCommandHandler.cs
Line coverage
100%
Covered lines: 39
Uncovered lines: 0
Coverable lines: 39
Total lines: 85
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
Branch coverage: 100%
Method coverage
100%
Covered methods: 3
Fully covered methods: 3
Total methods: 3
Method coverage: 100%
Full method coverage: 100%

Metrics

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

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Application/ArturRios.Heimdall.Command/Handlers/SetScopeEmailLinksCommandHandler.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="SetScopeEmailLinksCommand" />: the same order of checks as
 17///     <see cref="SetSelfRegistrationCommandHandler" /> — the links are usable, the scope is live, a
 18///     Scope Admin actor owns it — then writes <c>EmailVerificationUrl</c> and <c>PasswordResetUrl</c>
 19///     and returns the scope. Nothing else on the scope is touched.
 20/// </summary>
 1721public class SetScopeEmailLinksCommandHandler(
 1722    IValidator<SetScopeEmailLinksCommand> validator,
 1723    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 1724    IAtomicWrites atomicWrites,
 1725    IScopeOwnershipChecker scopeOwnership)
 26    : ICommandHandlerAsync<SetScopeEmailLinksCommand, SetScopeEmailLinksCommandOutput>
 27{
 28    public async Task<DataOutput<SetScopeEmailLinksCommandOutput?>> HandleAsync(
 29        SetScopeEmailLinksCommand command, CancellationToken cancellationToken = default)
 30    {
 1731        var output = DataOutput<SetScopeEmailLinksCommandOutput?>.New;
 32
 1733        var validation = await validator.ValidateAsync(command, cancellationToken);
 34
 1735        if (!validation.IsValid)
 36        {
 437            return output.WithErrors(validation.Errors.Select(failure => failure.ErrorMessage));
 38        }
 39
 1540        var scope = await scopeReader.Query()
 1541            .FirstOrDefaultAsync(x => x.PublicId == command.Id && !x.IsDeleted, cancellationToken);
 42
 1543        if (scope is null)
 44        {
 345            return output.WithError(ScopeMessages.ScopeNotFound);
 46        }
 47
 1248        if (!await scopeOwnership.ActorMayManageScopeAsync(command.ActingRole, command.ActingPersonId, scope.Id))
 49        {
 150            return output.WithError(ScopeMessages.NotScopeOwner);
 51        }
 52
 1153        var ownerIds = await scopeReader.Query()
 1154            .Where(x => x.PublicId == command.Id)
 1155            .SelectMany(x => x.Owners.Select(owner => owner.Person.PublicId))
 1156            .ToListAsync(cancellationToken);
 57
 58        // The two links alone, and only while the scope is not deleted (#205), as UC-24 writes its flag:
 59        // one deleted since the read above is answered as one deleted before it.
 1160        if (!await atomicWrites.TrySetEmailLinksAsync(scope, Normalised(command.EmailVerificationUrl),
 1161                Normalised(command.PasswordResetUrl), DateTime.UtcNow))
 62        {
 163            return output.WithError(ScopeMessages.ScopeNotFound);
 64        }
 65
 1066        return output
 1067            .WithData(new SetScopeEmailLinksCommandOutput
 1068            {
 1069                Id = scope.PublicId,
 1070                Name = scope.Name,
 1071                Description = scope.Description,
 1072                GoogleSignInEnabled = scope.GoogleSignInEnabled,
 1073                SelfRegistrationEnabled = scope.SelfRegistrationEnabled,
 1074                EmailVerificationUrl = scope.EmailVerificationUrl,
 1075                PasswordResetUrl = scope.PasswordResetUrl,
 1076                OwnerIds = ownerIds,
 1077                CreatedAt = scope.CreatedAt,
 1078                UpdatedAt = scope.UpdatedAt
 1079            })
 1080            .WithMessage(ScopeMessages.EmailLinksUpdatedSuccessfully);
 1781    }
 82
 83    // An empty or blank link is stored as "no link", so the senders have one way to read it.
 2284    private static string? Normalised(string? url) => string.IsNullOrWhiteSpace(url) ? null : url.Trim();
 85}