using MicCheck.Api.Common.Security.Authorization; using MicCheck.Api.Data; using MicCheck.Api.Organizations; using MicCheck.Api.Tests.Unit.TestSupport; using MicCheck.Api.Users; using Microsoft.AspNetCore.Identity; using Moq; using NUnit.Framework; namespace MicCheck.Api.Tests.Unit.Common.Security.Authorization; [TestFixture] public class AuthServiceTests { private Mock _db = null!; private List _users = null!; private List _organizations = null!; private List _organizationUsers = null!; private List _refreshTokens = null!; private PasswordHasher _passwordHasher = null!; private AuthService _service = null!; [SetUp] public void SetUp() { _db = new Mock(); _users = []; _db.SetupDbSetWithGeneratedIds(c => c.Users, _users); _organizations = []; _db.SetupDbSetWithGeneratedIds(c => c.Organizations, _organizations); _organizationUsers = []; _db.SetupDbSet(c => c.OrganizationUsers, _organizationUsers); _refreshTokens = []; _db.SetupDbSetWithGeneratedIds(c => c.RefreshTokens, _refreshTokens); _passwordHasher = new PasswordHasher(); var tokenService = new Mock(); tokenService.Setup(t => t.GenerateToken(It.IsAny())) .Returns(new TokenResponse("access-token", DateTime.UtcNow.AddHours(1))); _service = new AuthService(_db.Object, tokenService.Object, _passwordHasher); } private User AddUser(string email, string password, bool isActive = true) { var user = new User { Email = email, FirstName = "First", LastName = "Last", IsActive = isActive, CreatedAt = DateTimeOffset.UtcNow, PasswordHash = string.Empty }; user.PasswordHash = _passwordHasher.HashPassword(user, password); _db.Object.Users.Add(user); return user; } [Test] public async Task WhenRegisteringWithValidDetails_ThenTokensAreReturned() { var response = await _service.RegisterAsync( "alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"); Assert.That(response, Is.Not.Null); Assert.That(response!.AccessToken, Is.Not.Null.And.Not.Empty); Assert.That(response.RefreshToken, Is.Not.Null.And.Not.Empty); Assert.That(_users, Has.Count.EqualTo(1)); Assert.That(_organizations, Has.Count.EqualTo(1)); Assert.That(_organizationUsers.Single().Role, Is.EqualTo(OrganizationRole.Admin)); } [Test] public async Task WhenRegisteringWithDuplicateEmail_ThenNullIsReturned() { AddUser("alice@example.com", "SecurePass1!"); var response = await _service.RegisterAsync( "alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"); Assert.That(response, Is.Null); } [Test] public async Task WhenLoginWithValidCredentials_ThenTokensAreReturned() { AddUser("bob@example.com", "SecurePass1!"); var response = await _service.LoginAsync("bob@example.com", "SecurePass1!"); Assert.That(response, Is.Not.Null); Assert.That(response!.AccessToken, Is.Not.Null.And.Not.Empty); Assert.That(response.RefreshToken, Is.Not.Null.And.Not.Empty); } [Test] public async Task WhenLoginWithWrongPassword_ThenNullIsReturned() { AddUser("carol@example.com", "CorrectPass1!"); var response = await _service.LoginAsync("carol@example.com", "WrongPassword"); Assert.That(response, Is.Null); } [Test] public async Task WhenLoginWithUnknownEmail_ThenNullIsReturned() { var response = await _service.LoginAsync("nobody@example.com", "SomePass1!"); Assert.That(response, Is.Null); } [Test] public async Task WhenRefreshingWithValidToken_ThenNewTokensAreReturnedAndOldTokenIsRevoked() { var user = AddUser("dave@example.com", "SecurePass1!"); var refreshToken = new RefreshToken { Token = "valid-refresh-token", UserId = user.Id, User = user, ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), CreatedAt = DateTimeOffset.UtcNow }; _db.Object.RefreshTokens.Add(refreshToken); var response = await _service.RefreshAsync("valid-refresh-token"); Assert.That(response, Is.Not.Null); Assert.That(response!.RefreshToken, Is.Not.EqualTo("valid-refresh-token")); Assert.That(refreshToken.IsRevoked, Is.True); } [Test] public async Task WhenRefreshingWithInvalidToken_ThenNullIsReturned() { var response = await _service.RefreshAsync("not-a-valid-refresh-token"); Assert.That(response, Is.Null); } [Test] public async Task WhenRefreshingWithRevokedToken_ThenNullIsReturned() { var user = AddUser("eve@example.com", "SecurePass1!"); _db.Object.RefreshTokens.Add(new RefreshToken { Token = "revoked-token", UserId = user.Id, User = user, ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), IsRevoked = true, CreatedAt = DateTimeOffset.UtcNow }); var response = await _service.RefreshAsync("revoked-token"); Assert.That(response, Is.Null); } [Test] public async Task WhenRefreshingWithExpiredToken_ThenNullIsReturned() { var user = AddUser("frank@example.com", "SecurePass1!"); _db.Object.RefreshTokens.Add(new RefreshToken { Token = "expired-token", UserId = user.Id, User = user, ExpiresAt = DateTimeOffset.UtcNow.AddDays(-1), CreatedAt = DateTimeOffset.UtcNow.AddDays(-31) }); var response = await _service.RefreshAsync("expired-token"); Assert.That(response, Is.Null); } [Test] public async Task WhenLoggingOut_ThenTokenIsRevoked() { var user = AddUser("grace@example.com", "SecurePass1!"); var refreshToken = new RefreshToken { Token = "logout-token", UserId = user.Id, User = user, ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), CreatedAt = DateTimeOffset.UtcNow }; _db.Object.RefreshTokens.Add(refreshToken); await _service.LogoutAsync("logout-token"); Assert.That(refreshToken.IsRevoked, Is.True); } [Test] public async Task WhenLoggingOutAndRefreshing_ThenNullIsReturned() { var user = AddUser("henry@example.com", "SecurePass1!"); _db.Object.RefreshTokens.Add(new RefreshToken { Token = "logout-then-refresh-token", UserId = user.Id, User = user, ExpiresAt = DateTimeOffset.UtcNow.AddDays(30), CreatedAt = DateTimeOffset.UtcNow }); await _service.LogoutAsync("logout-then-refresh-token"); var response = await _service.RefreshAsync("logout-then-refresh-token"); Assert.That(response, Is.Null); } [Test] public void WhenLoggingOutWithATokenThatDoesNotExist_ThenNoExceptionIsThrown() { Assert.DoesNotThrowAsync(() => _service.LogoutAsync("unknown-token")); } }