Files
Amaury Levé 948a9a52ef Add exp-mock-usage-analysis skill and evaluation (#483)
* Add exp-mock-usage-analysis skill and evaluation

New experimental skill that detects unused, redundant, or unnecessary
mocks in .NET test suites. Covers Moq, NSubstitute, FakeItEasy, and
manual test doubles.

- SKILL.md with 6-step workflow: gather code, classify mocks, detect
  anti-patterns, incorporate runtime data, generate recommendations,
  report findings
- eval.yaml with 10 scenarios: trivial type mocking, unused setups,
  redundant configs, stable utility mocks, well-placed mocks (no false
  positives), NSubstitute support, FakeItEasy support, setup sprawl,
  and 2 non-activation boundary tests
- Fixture files for each scenario with production + test code
- CODEOWNERS entry for @dotnet/dotnet-testing

* Remove no-headroom scenarios, increase NSubstitute timeout

Remove 4 scenarios where baseline already scores 5.0/5 (model handles
these well without skill guidance):
- Detect mocking of DTOs, records, and enums
- Recognize well-placed mocks without inventing false positives
- Decline when asked about non-mock test anti-patterns
- Decline mock framework migration request

Increase NSubstitute scenario timeout from 120s to 180s to avoid
baseline timeouts impacting scoring.

Remaining 6 scenarios all show positive skill impact (3→5, 3→4, etc).

* Document areas the model handles well without the skill

Add a subsection under 'When Not to Use' listing mock-related tasks
where the base model already scores 5.0/5 without skill guidance:
- Mocking trivial types (DTOs, records, enums)
- Recognizing well-placed mocks (no false positives)

* Fix md

* Improve
2026-04-02 10:03:17 +00:00

76 lines
2.2 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace UserManagement;
public class UserManagerOptions
{
public int MaxLoginAttempts { get; set; } = 5;
public TimeSpan LockoutDuration { get; set; } = TimeSpan.FromMinutes(15);
}
public interface IUserStore
{
User? FindByEmail(string email);
void Update(User user);
}
public interface IPasswordHasher
{
bool Verify(string password, string hash);
}
public record User(int Id, string Email, string PasswordHash, int FailedAttempts, DateTime? LockedUntil);
public class AuthenticationService
{
private readonly IUserStore _userStore;
private readonly IPasswordHasher _hasher;
private readonly ILogger<AuthenticationService> _logger;
private readonly IOptions<UserManagerOptions> _options;
public AuthenticationService(
IUserStore userStore,
IPasswordHasher hasher,
ILogger<AuthenticationService> logger,
IOptions<UserManagerOptions> options)
{
_userStore = userStore;
_hasher = hasher;
_logger = logger;
_options = options;
}
public bool Login(string email, string password)
{
var user = _userStore.FindByEmail(email);
if (user is null)
{
_logger.LogWarning("Login attempt for unknown email: {Email}", email);
return false;
}
if (user.LockedUntil.HasValue && user.LockedUntil > DateTime.UtcNow)
{
_logger.LogWarning("Login attempt for locked account: {Email}", email);
return false;
}
if (!_hasher.Verify(password, user.PasswordHash))
{
var attempts = user.FailedAttempts + 1;
DateTime? lockUntil = attempts >= _options.Value.MaxLoginAttempts
? DateTime.UtcNow.Add(_options.Value.LockoutDuration)
: null;
_userStore.Update(user with { FailedAttempts = attempts, LockedUntil = lockUntil });
_logger.LogWarning("Failed login for {Email}. Attempt {Attempt}", email, attempts);
return false;
}
_userStore.Update(user with { FailedAttempts = 0, LockedUntil = null });
_logger.LogInformation("Successful login for {Email}", email);
return true;
}
}