mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +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()
|
||||
{
|
||||
var service = new Mock<IUserService>();
|
||||
service.Setup(s => s.DeleteUserAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
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);
|
||||
var controller = NewController(service, "admin-1", "Admin");
|
||||
|
||||
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()
|
||||
{
|
||||
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);
|
||||
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -61,16 +61,17 @@ public class UserServiceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUser_NotFound_ReturnsFalseWithoutCascade()
|
||||
public async Task DeleteUser_NotFound_ReturnsUserNotFoundWithoutCascade()
|
||||
{
|
||||
var userData = new Mock<IUserDataService>();
|
||||
userData.Setup(u => u.GetForEditAsync("missing", It.IsAny<CancellationToken>())).ReturnsAsync((ApplicationUser?)null);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -83,12 +84,28 @@ public class UserServiceTests
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task DeleteCurrentUser_SetsIsDeletedFlag()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -94,14 +94,17 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
|
||||
[HttpDelete]
|
||||
[Route("DeleteUser")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> DeleteUser(EditUser_DTO req, CancellationToken cancellationToken)
|
||||
{
|
||||
string id = req.Id ?? "";
|
||||
try
|
||||
{
|
||||
var found = await _userService.DeleteUserAsync(id, cancellationToken);
|
||||
if (!found)
|
||||
var outcome = await _userService.DeleteUserAsync(id, User, cancellationToken);
|
||||
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 Ok(new DataResponse { Message = "Updated Successfully", Status = "200" });
|
||||
|
|
|
|||
|
|
@ -161,14 +161,21 @@ namespace SeaHaven.Services.Implementation
|
|||
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);
|
||||
if (data == null)
|
||||
return false;
|
||||
return new AddUserOutcomeDTO { Success = false, Error = UserMutationErrors.UserNotFound };
|
||||
|
||||
await _userDataService.DeleteUserWithCascadeAsync(data, cancellationToken);
|
||||
return true;
|
||||
return new AddUserOutcomeDTO { Success = true };
|
||||
}
|
||||
|
||||
public async Task DeleteCurrentUserAsync(string userId, CancellationToken cancellationToken)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task<IEnumerable<UserListRowDTO>> GetUsersAsync(CancellationToken cancellationToken);
|
||||
Task<AddUserOutcomeDTO> AddUserAsync(AddUserRequestDTO 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<IReadOnlyList<UserProfileRowDTO>> GetUserProfileAsync(string userId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue