mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 11:39:03 +00:00
!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>
This commit is contained in:
parent
d0724a0ac5
commit
446e43b2c6
5 changed files with 71 additions and 17 deletions
|
|
@ -183,10 +183,13 @@ public class UserControllerTests
|
||||||
public async Task DeleteUser_NotFound_ReturnsIdNotMatchedMessage()
|
public async Task DeleteUser_NotFound_ReturnsIdNotMatchedMessage()
|
||||||
{
|
{
|
||||||
var service = new Mock<IUserService>();
|
var service = new Mock<IUserService>();
|
||||||
service.Setup(s => s.DeleteUserAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
service.Setup(s => s.DeleteUserAsync(
|
||||||
.ReturnsAsync(false);
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<ClaimsPrincipal>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound });
|
||||||
|
|
||||||
var controller = NewController(service);
|
var controller = NewController(service, "admin-1", "Admin");
|
||||||
|
|
||||||
var result = await controller.DeleteUser(new EditUser_DTO { Id = "missing" }, CancellationToken.None);
|
var result = await controller.DeleteUser(new EditUser_DTO { Id = "missing" }, CancellationToken.None);
|
||||||
|
|
||||||
|
|
@ -199,14 +202,38 @@ public class UserControllerTests
|
||||||
public async Task DeleteUser_Found_DelegatesAndReturnsOk()
|
public async Task DeleteUser_Found_DelegatesAndReturnsOk()
|
||||||
{
|
{
|
||||||
var service = new Mock<IUserService>();
|
var service = new Mock<IUserService>();
|
||||||
service.Setup(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>())).ReturnsAsync(true);
|
service.Setup(s => s.DeleteUserAsync(
|
||||||
|
"5",
|
||||||
|
It.IsAny<ClaimsPrincipal>(),
|
||||||
|
It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new AddUserOutcomeDTO { Success = true });
|
||||||
|
|
||||||
var controller = NewController(service);
|
var controller = NewController(service, "admin-1", "Admin");
|
||||||
|
|
||||||
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
|
var result = await controller.DeleteUser(new EditUser_DTO { Id = "5" }, CancellationToken.None);
|
||||||
|
|
||||||
result.Should().BeOfType<OkObjectResult>();
|
result.Should().BeOfType<OkObjectResult>();
|
||||||
service.Verify(s => s.DeleteUserAsync("5", It.IsAny<CancellationToken>()), Times.Once);
|
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]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -61,16 +61,17 @@ public class UserServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteUser_NotFound_ReturnsFalseWithoutCascade()
|
public async Task DeleteUser_NotFound_ReturnsUserNotFoundWithoutCascade()
|
||||||
{
|
{
|
||||||
var userData = new Mock<IUserDataService>();
|
var userData = new Mock<IUserDataService>();
|
||||||
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
||||||
|
|
||||||
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
||||||
|
|
||||||
var found = await service.DeleteUserAsync("missing", CancellationToken.None);
|
var outcome = await service.DeleteUserAsync("missing", Principal("Admin"), CancellationToken.None);
|
||||||
|
|
||||||
found.Should().BeFalse();
|
outcome.Success.Should().BeFalse();
|
||||||
|
outcome.Error.Should().Be(UserMutationErrors.UserNotFound);
|
||||||
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,12 +84,28 @@ public class UserServiceTests
|
||||||
|
|
||||||
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
||||||
|
|
||||||
var found = await service.DeleteUserAsync("u1", CancellationToken.None);
|
var outcome = await service.DeleteUserAsync("u1", Principal("Admin"), CancellationToken.None);
|
||||||
|
|
||||||
found.Should().BeTrue();
|
outcome.Success.Should().BeTrue();
|
||||||
userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteUser_NonAdmin_ReturnsForbiddenWithoutCascade()
|
||||||
|
{
|
||||||
|
var user = IdentityTestHelpers.User();
|
||||||
|
var userData = new Mock<IUserDataService>();
|
||||||
|
userData.Setup(u => u.GetForEditAsync("u1", It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
||||||
|
|
||||||
|
var service = NewService(userData, new Mock<IEmailSender>(), out _);
|
||||||
|
|
||||||
|
var outcome = await service.DeleteUserAsync("u1", Principal("User"), CancellationToken.None);
|
||||||
|
|
||||||
|
outcome.Success.Should().BeFalse();
|
||||||
|
outcome.Error.Should().Be(UserMutationErrors.Forbidden);
|
||||||
|
userData.Verify(u => u.DeleteUserWithCascadeAsync(It.IsAny<ApplicationUser>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
|
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -94,14 +94,17 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
[HttpDelete]
|
[HttpDelete]
|
||||||
[Route("DeleteUser")]
|
[Route("DeleteUser")]
|
||||||
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> DeleteUser(EditUser_DTO req, CancellationToken cancellationToken)
|
public async Task<IActionResult> DeleteUser(EditUser_DTO req, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
string id = req.Id ?? "";
|
string id = req.Id ?? "";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var found = await _userService.DeleteUserAsync(id, cancellationToken);
|
var outcome = await _userService.DeleteUserAsync(id, User, cancellationToken);
|
||||||
if (!found)
|
if (!outcome.Success)
|
||||||
{
|
{
|
||||||
|
if (string.Equals(outcome.Error, UserMutationErrors.Forbidden, StringComparison.Ordinal))
|
||||||
|
return Forbid();
|
||||||
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
|
return BadRequest(new Response { Status = "Error", Message = "ID not matched!" });
|
||||||
}
|
}
|
||||||
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
return Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||||
|
|
|
||||||
|
|
@ -161,14 +161,21 @@ namespace SeaHaven.Services.Implementation
|
||||||
return new AddUserOutcomeDTO { Success = true };
|
return new AddUserOutcomeDTO { Success = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken)
|
public async Task<AddUserOutcomeDTO> DeleteUserAsync(
|
||||||
|
string id,
|
||||||
|
ClaimsPrincipal caller,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var authFailure = EnsureAdminCaller(caller);
|
||||||
|
if (authFailure != null)
|
||||||
|
return authFailure;
|
||||||
|
|
||||||
var data = await _userDataService.GetForEditAsync(id, cancellationToken);
|
var data = await _userDataService.GetForEditAsync(id, cancellationToken);
|
||||||
if (data == null)
|
if (data == null)
|
||||||
return false;
|
return new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound };
|
||||||
|
|
||||||
await _userDataService.DeleteUserWithCascadeAsync(data, cancellationToken);
|
await _userDataService.DeleteUserWithCascadeAsync(data, cancellationToken);
|
||||||
return true;
|
return new AddUserOutcomeDTO { Success = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken)
|
public async Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace SeaHaven.Services.Interfaces
|
||||||
Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken);
|
Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken);
|
||||||
Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
|
Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
|
||||||
Task<AddUserOutcomeDTO> EditUserAsync(EditUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
|
Task<AddUserOutcomeDTO> EditUserAsync(EditUserRequestDTO dto, ClaimsPrincipal caller, CancellationToken cancellationToken);
|
||||||
Task<bool> DeleteUserAsync(string id, CancellationToken cancellationToken);
|
Task<AddUserOutcomeDTO> DeleteUserAsync(string id, ClaimsPrincipal caller, CancellationToken cancellationToken);
|
||||||
Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken);
|
Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken);
|
||||||
Task<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken);
|
Task<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue