using Omika.Diagnostics.Web.Authorization; namespace Omika.Diagnostics.Tests; /// T23 — Anti-bruteforce léger (in-memory) : blocage temporaire après N échecs. public class LoginThrottleTests { private static readonly DateTimeOffset Now = new(2025, 1, 1, 12, 0, 0, TimeSpan.Zero); [Fact] public void BlocksOnlyAfterMaxAttempts() { var throttle = new LoginThrottle(); string key = LoginThrottle.Key("alice", "127.0.0.1"); for (int i = 0; i < LoginThrottle.MaxAttempts; i++) { throttle.RecordFailure(key, Now.AddSeconds(i)); } Assert.True(throttle.IsBlocked(key, Now.AddSeconds(LoginThrottle.MaxAttempts))); } [Fact] public void NotBlockedBelowThreshold() { var throttle = new LoginThrottle(); string key = LoginThrottle.Key("alice", "127.0.0.1"); for (int i = 0; i < LoginThrottle.MaxAttempts - 1; i++) { throttle.RecordFailure(key, Now.AddSeconds(i)); } Assert.False(throttle.IsBlocked(key, Now.AddSeconds(LoginThrottle.MaxAttempts))); } [Fact] public void LockoutExpires() { var throttle = new LoginThrottle(); string key = LoginThrottle.Key("alice", "127.0.0.1"); for (int i = 0; i < LoginThrottle.MaxAttempts; i++) { throttle.RecordFailure(key, Now.AddSeconds(i)); } Assert.True(throttle.IsBlocked(key, Now.AddSeconds(60))); Assert.False(throttle.IsBlocked(key, Now + LoginThrottle.Lockout + TimeSpan.FromSeconds(1))); } [Fact] public void ResetClearsFailures() { var throttle = new LoginThrottle(); string key = LoginThrottle.Key("alice", "127.0.0.1"); for (int i = 0; i < LoginThrottle.MaxAttempts; i++) { throttle.RecordFailure(key, Now.AddSeconds(i)); } throttle.Reset(key); Assert.False(throttle.IsBlocked(key, Now.AddSeconds(60))); } [Fact] public void KeyIsInsensitiveToCaseAndTrims() { Assert.Equal( LoginThrottle.Key(" Alice ", "127.0.0.1"), LoginThrottle.Key("alice", "127.0.0.1")); } [Fact] public void KeySeparatesIps() { Assert.NotEqual( LoginThrottle.Key("alice", "127.0.0.1"), LoginThrottle.Key("alice", "10.0.0.1")); } }