shoc-backend/Api.SeaHavenIndustries.Tests/UserControllerTests.cs
Arthur Bassi 446e43b2c6 !fix(users): restrict DeleteUser to Admin [SH-221]
Align DeleteUser with AddUser/EditUser: Admin role at controller and
service entry, Forbidden for non-Admin, and regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 09:44:52 -03:00

276 lines
10 KiB
C#

using Api.SeaHavenIndustries.Controllers;
using Api.SeaHavenIndustries.DTOs;
using Data.SeaHavenIndustries;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Interfaces;
using System.Security.Claims;
using System.Text.Json;
using Xunit;
namespace Api.SeaHavenIndustries.Tests;
public class UserControllerTests
{
private static string Json(object? value) =>
JsonSerializer.Serialize(value, value?.GetType() ?? typeof(object));
private static UserController NewController(
Mock<IUserService> service,
string? userId = null,
string? role = null)
{
var controller = new UserController(service.Object, Mock.Of<ILogger<UserController>>());
var claims = new List<Claim>();
if (userId != null)
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId));
if (role != null)
claims.Add(new Claim(ClaimTypes.Role, role));
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test"))
}
};
return controller;
}
[Fact]
public async Task GetUsers_ReturnsServiceList()
{
var service = new Mock<IUserService>();
service.Setup(s => s.GetUsersAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UserListRowDTO>
{
new() { Id = "1", Name = "A", Email = "a@x.com", RoleName = "Admin", Status = "Active", Date = "Jan 01 2026" }
});
var controller = NewController(service);
var result = await controller.GetUsers(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeAssignableTo<IEnumerable<UserListRowDTO>>();
}
[Fact]
public async Task AddUser_Success_ReturnsUpdatedSuccessfully()
{
var service = new Mock<IUserService>();
service.Setup(s => s.AddUserAsync(
It.IsAny<AddUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.AddUser(new User_DTO { Name = "N", Email = "n@x.com", Role = "Admin" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var response = ok.Value.Should().BeOfType<DataResponse>().Subject;
response.Status.Should().Be("200");
response.Message.Should().Be("Updated Successfully");
service.Verify(s => s.AddUserAsync(
It.IsAny<AddUserRequestDTO>(),
It.Is<ClaimsPrincipal>(p => p.IsInRole("Admin")),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task AddUser_Failure_ReturnsBadRequestWithServiceError()
{
var service = new Mock<IUserService>();
service.Setup(s => s.AddUserAsync(
It.IsAny<AddUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = "boom" });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.AddUser(new User_DTO(), CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Status.Should().Be("Error");
response.Message.Should().Be("boom");
}
[Fact]
public async Task AddUser_ServiceForbidden_ReturnsForbid()
{
var service = new Mock<IUserService>();
service.Setup(s => s.AddUserAsync(
It.IsAny<AddUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.Forbidden });
var controller = NewController(service, "tech-1", "User");
var result = await controller.AddUser(
new User_DTO { Name = "N", Email = "n@x.com", Role = "User", AccountId = 99 },
CancellationToken.None);
result.Should().BeOfType<ForbidResult>();
}
[Fact]
public async Task EditUser_NotFound_ReturnsUserNotFoundMessage()
{
var service = new Mock<IUserService>();
service.Setup(s => s.EditUserAsync(
It.IsAny<EditUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.EditUser(new EditUser_DTO { Id = "9" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be(UserMutationErrors.UserNotFound);
}
[Fact]
public async Task EditUser_Found_ReturnsUpdatedSuccessfully()
{
var service = new Mock<IUserService>();
service.Setup(s => s.EditUserAsync(
It.IsAny<EditUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.EditUser(new EditUser_DTO { Id = "9" }, CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeOfType<DataResponse>();
}
[Fact]
public async Task EditUser_ServiceForbidden_ReturnsForbid()
{
var service = new Mock<IUserService>();
service.Setup(s => s.EditUserAsync(
It.IsAny<EditUserRequestDTO>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.Forbidden });
var controller = NewController(service, "tech-1", "User");
var result = await controller.EditUser(
new EditUser_DTO { Id = "tech-1", AccountId = 42, Email = "me@x.com", Role = "User" },
CancellationToken.None);
result.Should().BeOfType<ForbidResult>();
}
[Fact]
public async Task DeleteUser_NotFound_ReturnsIdNotMatchedMessage()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(
It.IsAny<string>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "missing" }, CancellationToken.None);
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
var response = bad.Value.Should().BeOfType<Response>().Subject;
response.Message.Should().Be("ID not matched!");
}
[Fact]
public async Task DeleteUser_Found_DelegatesAndReturnsOk()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(
"5",
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
var controller = NewController(service, "admin-1", "Admin");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
service.Verify(s => s.DeleteUserAsync(
"5",
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task DeleteUser_Forbidden_ReturnsForbid()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteUserAsync(
It.IsAny<string>(),
It.IsAny<ClaimsPrincipal>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.Forbidden });
var controller = NewController(service, "tech-1", "User");
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
result.Should().BeOfType<ForbidResult>();
}
[Fact]
public async Task DeleteCurrentUser_UsesClaimUserId()
{
var service = new Mock<IUserService>();
service.Setup(s => s.DeleteCurrentUserAsync("77", It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
var controller = NewController(service, "77");
var result = await controller.DeleteCurrentUser(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
ok.Value.Should().BeOfType<DataResponse>();
service.Verify(s => s.DeleteCurrentUserAsync("77", It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task UserProfile_PreservesLowercaseAddedDateKey()
{
var service = new Mock<IUserService>();
service.Setup(s => s.GetUserProfileAsync("77", It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<UserProfileRowDTO>
{
new() { Id = "77", Name = "Me", AddedDate = new DateTime(2026, 1, 1), Email = "me@x.com", Contact = "321" }
});
var controller = NewController(service, "77");
var result = await controller.UserProfile(CancellationToken.None);
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
var json = Json(ok.Value);
json.Should().Contain("\"Id\":\"77\"");
json.Should().Contain("\"Name\":\"Me\"");
json.Should().Contain("\"addeddate\":\"");
json.Should().Contain("\"Email\":\"me@x.com\"");
json.Should().Contain("\"Contact\":\"321\"");
}
}