< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Command.Services.TwoFactorEmailCodeIssuer
Assembly: ArturRios.Heimdall.Command
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Application/ArturRios.Heimdall.Command/Services/TwoFactorEmailCodeIssuer.cs
Line coverage
92%
Covered lines: 37
Uncovered lines: 3
Coverable lines: 40
Total lines: 83
Line coverage: 92.5%
Branch coverage
62%
Covered branches: 5
Total branches: 8
Branch coverage: 62.5%
Method coverage
100%
Covered methods: 3
Fully covered methods: 1
Total methods: 3
Method coverage: 100%
Full method coverage: 33.3%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ReissueAsync()50%4492%
RetireOutstandingAsync()75%4490.9%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Shared.Security;
 4using ArturRios.Util.Random;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace ArturRios.Heimdall.Command.Services;
 8
 9/// <inheritdoc cref="ITwoFactorEmailCodeIssuer" />
 22810public class TwoFactorEmailCodeIssuer(
 22811    IAsyncReadOnlyRepository<TwoFactorEmailCode, long> emailCodeReader,
 22812    IAsyncRepository<TwoFactorEmailCode, long> emailCodeWriter,
 22813    ITwoFactorEmailSender emailSender) : ITwoFactorEmailCodeIssuer
 14{
 15    private const int CodeLength = 6;
 16
 17    public async Task<IEnumerable<string>?> ReissueAsync(TwoFactorAuth twoFactorAuth, string email)
 18    {
 5219        var retirement = await RetireOutstandingAsync(twoFactorAuth.Id);
 20
 5221        if (retirement is not null)
 22        {
 023            return retirement;
 24        }
 25
 5226        var code = CustomRandom.Text(new RandomStringOptions
 5227        {
 5228            Length = CodeLength,
 5229            IncludeDigits = true,
 5230            IncludeLowercase = false,
 5231            IncludeUppercase = false,
 5232            IncludeSpecialCharacters = false
 5233        });
 34
 35        // Under the process-wide Argon2id bound (TH-03), as every derivation on a request path is.
 5236        var (codeHash, salt) = await PasswordHashGate.Shared.EncodeWithRandomSaltAsync(code);
 37
 5238        var creation = await emailCodeWriter.CreateAsync(new TwoFactorEmailCode
 5239        {
 5240            TwoFactorAuthId = twoFactorAuth.Id,
 5241            CodeHash = codeHash,
 5242            Salt = salt,
 5243            ExpiresAt = DateTime.UtcNow.Add(TwoFactorLifetimes.EmailCode),
 5244            Used = false
 5245        });
 46
 5247        if (!creation.Success)
 48        {
 049            return creation.Errors;
 50        }
 51
 52        // Delivery is attempted only after the code is persisted, and its outcome is deliberately
 53        // not reported. A caller who receives nothing asks for another (UC-46) or logs in again;
 54        // telling them the send failed would say something about the address, and telling them it
 55        // succeeded would not be true of the inbox anyway.
 5256        await emailSender.SendAsync(email, code);
 57
 5258        return null;
 5259    }
 60
 61    public async Task<IEnumerable<string>?> RetireOutstandingAsync(long twoFactorAuthId)
 62    {
 7363        var now = DateTime.UtcNow;
 64
 7365        var live = await emailCodeReader.Query()
 7366            .Where(x => x.TwoFactorAuthId == twoFactorAuthId && !x.Used && x.ExpiresAt > now)
 7367            .ToListAsync();
 68
 18869        foreach (var outstanding in live)
 70        {
 2171            outstanding.Used = true;
 72
 2173            var retirement = await emailCodeWriter.UpdateAsync(outstanding);
 74
 2175            if (!retirement.Success)
 76            {
 077                return retirement.Errors;
 78            }
 79        }
 80
 7381        return null;
 7382    }
 83}