Files
mic-check/tests/api/MicCheck.Api.Tests.Unit/Common/Security/Authorization/AuthServiceTests.cs
James Wampler 87113ccdcd
All checks were successful
CI / build-and-push (push) Successful in 54s
CI / deploy-qa (push) Successful in 12s
CI / smoke-qa (push) Successful in 24s
Add unit tests for Environments, Audit, and Webhooks namespaces (#6)
## Summary
- Add missing unit test coverage for the Environments, Audit, and Webhooks namespaces (raises them from ~0-62% to 91-100%)
- Exclude WebhookBackgroundService/WebhookRetryBackgroundService from coverage (require live DI/DB, disallowed by CLAUDE.md's no-InMemory/WebApplicationFactory rule)

## Test plan
- [x] `dotnet test` full suite passes (646/646)
- [x] Coverage report confirms Environments ~99.5%, Audit 100%, Webhooks ~91%

Reviewed-on: #6
2026-07-05 15:07:26 -07:00

226 lines
7.3 KiB
C#

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<IMicCheckDbContext> _db = null!;
private List<User> _users = null!;
private List<Organization> _organizations = null!;
private List<OrganizationUser> _organizationUsers = null!;
private List<RefreshToken> _refreshTokens = null!;
private PasswordHasher<User> _passwordHasher = null!;
private AuthService _service = null!;
[SetUp]
public void SetUp()
{
_db = new Mock<IMicCheckDbContext>();
_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<User>();
var tokenService = new Mock<ITokenService>();
tokenService.Setup(t => t.GenerateToken(It.IsAny<User>()))
.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"));
}
}