Add unit tests for Environments, Audit, and Webhooks namespaces (#6)
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

## 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
This commit was merged in pull request #6.
This commit is contained in:
2026-07-05 15:07:26 -07:00
parent cae55e5737
commit 87113ccdcd
64 changed files with 6447 additions and 65 deletions

View File

@@ -0,0 +1,217 @@
using System.Security.Claims;
using MicCheck.Api.Data;
using MicCheck.Api.Tests.Unit.TestSupport;
using MicCheck.Api.Users;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Moq;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Users;
[TestFixture]
public class UsersControllerTests
{
private Mock<IMicCheckDbContext> _db = null!;
private List<User> _users = null!;
private UsersController _controller = null!;
private const int UserId = 1;
[SetUp]
public void SetUp()
{
_db = new Mock<IMicCheckDbContext>();
_users = [];
_db.SetupDbSet(c => c.Users, _users);
var passwordHasher = new Mock<IPasswordHasher<User>>();
passwordHasher
.Setup(h => h.VerifyHashedPassword(It.IsAny<User>(), "hashed", "correct-password"))
.Returns(PasswordVerificationResult.Success);
passwordHasher
.Setup(h => h.VerifyHashedPassword(It.IsAny<User>(), "hashed", It.Is<string>(p => p != "correct-password")))
.Returns(PasswordVerificationResult.Failed);
passwordHasher
.Setup(h => h.HashPassword(It.IsAny<User>(), It.IsAny<string>()))
.Returns("new-hashed");
var service = new UserService(_db.Object, passwordHasher.Object);
_controller = new UsersController(service);
}
private User AddUser(int id = UserId, string email = "alice@example.com") => new()
{
Id = id,
Email = email,
PasswordHash = "hashed",
FirstName = "Alice",
LastName = "Smith",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
};
private void AuthenticateAs(int? userId)
{
var claims = userId is null ? [] : new[] { new Claim(ClaimTypes.NameIdentifier, userId.Value.ToString()) };
var identity = new ClaimsIdentity(claims, userId is null ? null : "TestAuth");
_controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(identity) }
};
}
[Test]
public async Task WhenGettingAUserById_ThenTheMatchingUserIsReturned()
{
var user = AddUser();
_users.Add(user);
var result = await _controller.GetById(user.Id, CancellationToken.None);
var ok = result.Result as OkObjectResult;
Assert.That(ok, Is.Not.Null);
Assert.That(((UserResponse)ok!.Value!).Id, Is.EqualTo(user.Id));
}
[Test]
public async Task WhenGettingAUserByIdThatDoesNotExist_ThenNotFoundIsReturned()
{
var result = await _controller.GetById(999, CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenGettingTheCurrentUserWithoutAuthentication_ThenUnauthorizedIsReturned()
{
AuthenticateAs(null);
var result = await _controller.GetMe(CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<UnauthorizedResult>());
}
[Test]
public async Task WhenGettingTheCurrentUser_ThenTheAuthenticatedUsersProfileIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.GetMe(CancellationToken.None);
var ok = result.Result as OkObjectResult;
Assert.That(ok, Is.Not.Null);
Assert.That(((UserResponse)ok!.Value!).Email, Is.EqualTo(user.Email));
}
[Test]
public async Task WhenUpdatingTheProfileWithoutAuthentication_ThenUnauthorizedIsReturned()
{
AuthenticateAs(null);
var result = await _controller.UpdateMe(new UpdateProfileRequest("Bob", "Jones", "bob@example.com"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<UnauthorizedResult>());
}
[Test]
public async Task WhenUpdatingTheProfileWithAMissingField_ThenBadRequestIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.UpdateMe(new UpdateProfileRequest("", "Jones", "bob@example.com"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task WhenUpdatingTheProfileWithAnEmailTakenByAnotherUser_ThenConflictIsReturned()
{
var user = AddUser(id: 1, email: "alice@example.com");
var otherUser = AddUser(id: 2, email: "taken@example.com");
_users.Add(user);
_users.Add(otherUser);
AuthenticateAs(user.Id);
var result = await _controller.UpdateMe(new UpdateProfileRequest("Alice", "Smith", "taken@example.com"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<ConflictObjectResult>());
}
[Test]
public async Task WhenUpdatingTheProfileForAUserThatNoLongerExists_ThenNotFoundIsReturned()
{
AuthenticateAs(999);
var result = await _controller.UpdateMe(new UpdateProfileRequest("Bob", "Jones", "bob@example.com"), CancellationToken.None);
Assert.That(result.Result, Is.InstanceOf<NotFoundResult>());
}
[Test]
public async Task WhenUpdatingTheProfileWithValidData_ThenTheUpdatedProfileIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.UpdateMe(new UpdateProfileRequest(" Bob ", " Jones ", " bob@example.com "), CancellationToken.None);
var ok = result.Result as OkObjectResult;
Assert.That(ok, Is.Not.Null);
var response = (UserResponse)ok!.Value!;
Assert.That(response.FirstName, Is.EqualTo("Bob"));
Assert.That(response.LastName, Is.EqualTo("Jones"));
Assert.That(response.Email, Is.EqualTo("bob@example.com"));
}
[Test]
public async Task WhenChangingPasswordWithoutAuthentication_ThenUnauthorizedIsReturned()
{
AuthenticateAs(null);
var result = await _controller.ChangePassword(new ChangePasswordRequest("current", "new"), CancellationToken.None);
Assert.That(result, Is.InstanceOf<UnauthorizedResult>());
}
[Test]
public async Task WhenChangingPasswordWithAMissingField_ThenBadRequestIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.ChangePassword(new ChangePasswordRequest("", "new"), CancellationToken.None);
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task WhenChangingPasswordWithAnIncorrectCurrentPassword_ThenBadRequestIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.ChangePassword(new ChangePasswordRequest("wrong-password", "new-password"), CancellationToken.None);
Assert.That(result, Is.InstanceOf<BadRequestObjectResult>());
}
[Test]
public async Task WhenChangingPasswordWithACorrectCurrentPassword_ThenNoContentIsReturned()
{
var user = AddUser();
_users.Add(user);
AuthenticateAs(user.Id);
var result = await _controller.ChangePassword(new ChangePasswordRequest("correct-password", "new-password"), CancellationToken.None);
Assert.That(result, Is.InstanceOf<NoContentResult>());
}
}