< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.PasswordResetService
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/PasswordResetService.cs
Line coverage
100%
Covered lines: 28
Uncovered lines: 0
Coverable lines: 28
Total lines: 66
Line coverage: 100%
Branch coverage
100%
Covered branches: 2
Total branches: 2
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%
IssueAndSendAsync()100%22100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/PasswordResetService.cs

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Util.Random;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace ArturRios.Heimdall.Command.Services;
 7
 8/// <summary>
 9///     Default <see cref="IPasswordResetService" /> (UC-12 steps 3 and 4, FR-PR-02): builds a
 10///     random, time-limited <see cref="PasswordResetToken" />, persists it, then delegates delivery
 11///     to the configured <see cref="IPasswordResetSender" />.
 12/// </summary>
 13/// <remarks>
 14///     <para>
 15///         The options ask for letters and digits only, matching <see cref="EmailVerificationService" />.
 16///         As of ArturRios.Util 1.5.0 <c>CustomRandom.Text</c> honours those flags for every character,
 17///         so the token is alphanumeric — and therefore URL-safe — throughout. The sender still escapes
 18///         the token before putting it in a link; nothing depends on that escaping being a no-op.
 19///     </para>
 20///     <para>
 21///         Secrecy comes from the source and the length: <c>CustomRandom.Text</c> draws from
 22///         <c>RandomNumberGenerator</c>, and 48 alphanumeric characters are far past guessing range
 23///         within the token's one-hour life.
 24///     </para>
 25/// </remarks>
 4626public class PasswordResetService(
 4627    IAsyncRepository<PasswordResetToken, long> tokenWriter,
 4628    IAsyncReadOnlyRepository<Scope, long> scopeReader,
 4629    IPasswordResetSender sender,
 4630    PasswordResetOptions options)
 31    : IPasswordResetService
 32{
 33    public async Task IssueAndSendAsync(Person person)
 34    {
 3235        var token = CustomRandom.Text(new RandomStringOptions
 3236        {
 3237            Length = 48,
 3238            IncludeLowercase = true,
 3239            IncludeUppercase = true,
 3240            IncludeDigits = true,
 3241            IncludeSpecialCharacters = false
 3242        });
 43
 3244        await tokenWriter.CreateAsync(new PasswordResetToken
 3245        {
 3246            PersonId = person.Id,
 3247            TokenHash = SingleUseTokenHash.Of(token),
 3248            ExpiresAt = DateTime.UtcNow.Add(options.TokenLifetime),
 3249            Used = false
 3250        });
 51
 52        // The person's scope may point the link at its own client application. An administrator has
 53        // no scope, and a scope with no link set leaves this null: both take the global setting.
 3254        string? linkBase = null;
 55
 3256        if (person.ScopeId is { } scopeId)
 57        {
 658            linkBase = await scopeReader.Query()
 659                .Where(scope => scope.Id == scopeId)
 660                .Select(scope => scope.PasswordResetUrl)
 661                .FirstOrDefaultAsync();
 62        }
 63
 3264        await sender.SendAsync(person.Email, token, linkBase);
 3265    }
 66}