< Summary

Information
Class: Anichron.API.Services.LockoutService
Assembly: Anichron.API
File(s): /home/runner/work/anichron/anichron/src/Anichron.API/Services/LockoutService.cs
Tag: 228_36821334185
Line coverage
100%
Covered lines: 17
Uncovered lines: 0
Coverable lines: 17
Total lines: 41
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
IsLockedOut(...)100%22100%
RecordFailedAttemptAsync()100%11100%
PrepareReset(...)100%11100%
ComputeBackoffSeconds(...)100%44100%

File(s)

/home/runner/work/anichron/anichron/src/Anichron.API/Services/LockoutService.cs

#LineLine coverage
 1using Anichron.API.Settings;
 2using Anichron.Core.Data;
 3using Anichron.Core.Domain;
 4
 5namespace Anichron.API.Services;
 6
 7public interface ILockoutService
 8{
 9    bool IsLockedOut(User user, Instant now);
 10    Task RecordFailedAttemptAsync(User user, Instant now, CancellationToken ct);
 11    // Mutates the entity only — the caller is responsible for persisting inside a transaction.
 12    void PrepareReset(User user);
 13}
 14
 1015public sealed class LockoutService(IUnitOfWork unitOfWork) : ILockoutService
 16{
 17    public bool IsLockedOut(User user, Instant now)
 418        => user.LockedUntil is { } lockedUntil && lockedUntil > now;
 19
 20    public async Task RecordFailedAttemptAsync(User user, Instant now, CancellationToken ct)
 521    {
 522        user.FailedLoginAttempts++;
 23        // When backoff = 0 (below threshold), LockedUntil = now is intentional — IsLockedOut uses strict >,
 24        // so the user is not locked out, but the timestamp is always written for consistency.
 525        user.LockedUntil = now.Plus(Duration.FromSeconds(ComputeBackoffSeconds(user.FailedLoginAttempts)));
 526        await unitOfWork.SaveChangesAsync(ct);
 527    }
 28
 29    public void PrepareReset(User user)
 130    {
 131        user.FailedLoginAttempts = 0;
 132        user.LockedUntil = null;
 133    }
 34
 1235    internal static int ComputeBackoffSeconds(int failedAttempts) => failedAttempts switch
 1236    {
 537        <= AppDefaults.Lockout.AllowedAttempts => 0,
 338        >= AppDefaults.Lockout.MaxAttempts => AppDefaults.Lockout.MaxSeconds,
 439        _ => (int)Math.Pow(AppDefaults.Lockout.BackoffBase, failedAttempts - AppDefaults.Lockout.AllowedAttempts),
 1240    };
 41}