< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Domain.Entities.AttemptNoticeBudget
Assembly: ArturRios.Heimdall.Domain
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Domain/ArturRios.Heimdall.Domain/Entities/AttemptNoticeBudget.cs
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 57
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 5
Fully covered methods: 5
Total methods: 5
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_AddressHash()100%11100%
get_NoticesSent()100%11100%
get_WindowEndsAt()100%11100%
AddressHashOf(...)100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Domain/ArturRios.Heimdall.Domain/Entities/AttemptNoticeBudget.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using ArturRios.Data.Relational.Core.Entities;
 4
 5namespace ArturRios.Heimdall.Domain.Entities;
 6
 7/// <summary>
 8///     How many registration-attempt notices (UC-47 AF-47b) one address has been sent in its current
 9///     window (FR-SR-14): at most <see cref="MaxNotices" /> a day, whichever scopes the attempts named.
 10/// </summary>
 11/// <remarks>
 12///     <para>
 13///         <b>Why it exists.</b> Registering with an address that is already taken mails its holder a
 14///         notice. The response is the same either way, so the caller cannot tell the notice went out
 15///         — but the holder can, once per request. The per-IP rate limit bounds one caller, not one
 16///         recipient: an attacker with many source addresses could otherwise fill an inbox with them.
 17///     </para>
 18///     <para>
 19///         <b>What it holds.</b> The address as a SHA-256 digest of its trimmed, lower-cased form —
 20///         never the address itself — the notices sent in the window, and when the window ends. The
 21///         first notice after a window has ended opens a new one. A row is purged with the
 22///         single-use tokens once its window has ended and the grace period passed (NFR-19), so no
 23///         row outlives its use by more than that.
 24///     </para>
 25///     <para>
 26///         The digest is pseudonymisation, not anonymisation: whoever already suspects an address can
 27///         hash it and look for it. It is personal data for that reason, and recorded as such in the
 28///         Data Protection Document.
 29///     </para>
 30/// </remarks>
 31public class AttemptNoticeBudget : Entity<long>
 32{
 33    /// <summary>The notices one address may be sent in a window.</summary>
 34    public const int MaxNotices = 3;
 35
 36    /// <summary>How long a window lasts, from the first notice sent in it.</summary>
 237    public static readonly TimeSpan Window = TimeSpan.FromDays(1);
 38
 39    /// <summary>
 40    ///     SHA-256 of the trimmed, lower-cased address, as lowercase hex (<see cref="AddressHashOf" />).
 41    ///     Unique: one row per address.
 42    /// </summary>
 1543    public string AddressHash { get; set; } = string.Empty;
 44
 45    /// <summary>Notices sent to the address since its current window opened.</summary>
 946    public int NoticesSent { get; set; }
 47
 48    /// <summary>When the current window ends; the next notice after it opens a new one.</summary>
 1249    public DateTime WindowEndsAt { get; set; }
 50
 51    /// <summary>
 52    ///     The digest an address is counted under. Case and surrounding spaces do not make another
 53    ///     address, as they do not for the scope uniqueness rule (FR-PE-09).
 54    /// </summary>
 55    public static string AddressHashOf(string address) =>
 3056        Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(address.Trim().ToLowerInvariant())));
 57}