mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 20:42:09 +00:00
fix(team-members): preserve update semantics and validate identity results (SH-326)
This commit is contained in:
parent
b9c916cb22
commit
a0b9fabb69
2 changed files with 87 additions and 8 deletions
|
|
@ -159,6 +159,14 @@ public sealed class TeamMemberServiceTests
|
||||||
userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
||||||
userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List<string> { "Dispatcher" });
|
userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List<string> { "Dispatcher" });
|
||||||
userManager.Setup(manager => manager.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null);
|
userManager.Setup(manager => manager.FindByEmailAsync("new@example.com")).ReturnsAsync((ApplicationUser?)null);
|
||||||
|
userManager.Setup(manager => manager.SetEmailAsync(user, "new@example.com"))
|
||||||
|
.Callback<ApplicationUser, string>((member, email) => member.Email = email)
|
||||||
|
.ReturnsAsync(IdentityResult.Success);
|
||||||
|
userManager.Setup(manager => manager.SetUserNameAsync(user, "new@example.com"))
|
||||||
|
.Callback<ApplicationUser, string>((member, email) => member.UserName = email)
|
||||||
|
.ReturnsAsync(IdentityResult.Success);
|
||||||
|
userManager.Setup(manager => manager.UpdateSecurityStampAsync(user))
|
||||||
|
.ReturnsAsync(IdentityResult.Success);
|
||||||
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
|
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
|
||||||
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>()))
|
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(new[] { "Central" });
|
.ReturnsAsync(new[] { "Central" });
|
||||||
|
|
@ -202,11 +210,60 @@ public sealed class TeamMemberServiceTests
|
||||||
result.Member.ServiceAreas.Should().Equal("Central");
|
result.Member.ServiceAreas.Should().Equal("Central");
|
||||||
result.Member.Permissions.Should().ContainSingle(permission => permission.PermissionKey == "createSites");
|
result.Member.Permissions.Should().ContainSingle(permission => permission.PermissionKey == "createSites");
|
||||||
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
userManager.Verify(manager => manager.UpdateSecurityStampAsync(user), Times.Once);
|
||||||
areas.Verify(data => data.ReplaceAsync(user.Id, It.Is<IReadOnlyCollection<string>>(value => value.SequenceEqual(new[] { "West" })), It.IsAny<CancellationToken>()), Times.Once);
|
areas.Verify(data => data.ReplaceAsync(user.Id, It.Is<IReadOnlyCollection<string>>(value => value.SequenceEqual(new[] { "West" })), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
overrides.Verify(data => data.ClearOverridesAsync(user.Id, It.IsAny<CancellationToken>()), Times.Once);
|
overrides.Verify(data => data.ClearOverridesAsync(user.Id, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
overrides.Verify(data => data.SetOverridesAsync(user.Id, It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["createSites"] == UserPermissionState.Deny), It.IsAny<CancellationToken>()), Times.Once);
|
overrides.Verify(data => data.SetOverridesAsync(user.Id, It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["createSites"] == UserPermissionState.Deny), It.IsAny<CancellationToken>()), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Update_LeavesOmittedOverridesUnchanged()
|
||||||
|
{
|
||||||
|
var service = NewService(
|
||||||
|
out var userManager,
|
||||||
|
out var roleManager,
|
||||||
|
out var areas,
|
||||||
|
out var overrides,
|
||||||
|
out var userData,
|
||||||
|
out var permissions);
|
||||||
|
var user = new ApplicationUser
|
||||||
|
{
|
||||||
|
Id = "dispatcher",
|
||||||
|
FirstName = "Taylor",
|
||||||
|
Email = "taylor@example.com",
|
||||||
|
Color = "#F59E0B",
|
||||||
|
UniqueName = "Active"
|
||||||
|
};
|
||||||
|
userManager.Setup(manager => manager.FindByIdAsync(user.Id)).ReturnsAsync(user);
|
||||||
|
userManager.Setup(manager => manager.GetRolesAsync(user)).ReturnsAsync(new List<string> { "Dispatcher" });
|
||||||
|
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
|
||||||
|
areas.Setup(data => data.GetForUserAsync(user.Id, It.IsAny<CancellationToken>())).ReturnsAsync(Array.Empty<string>());
|
||||||
|
permissions.Setup(data => data.GetProfileAsync(user.Id, It.IsAny<ClaimsPrincipal>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(TeamPermissionResult<TeamPermissionProfileDTO>.Success(new TeamPermissionProfileDTO
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Permissions = Array.Empty<TeamPermissionValueDTO>()
|
||||||
|
}));
|
||||||
|
|
||||||
|
var result = await service.UpdateAsync(
|
||||||
|
user.Id,
|
||||||
|
new UpdateTeamMemberRequestDTO
|
||||||
|
{
|
||||||
|
Name = "Taylor",
|
||||||
|
Role = "Dispatcher",
|
||||||
|
Color = "#F59E0B",
|
||||||
|
Email = user.Email,
|
||||||
|
ServiceAreas = new[] { "East" }
|
||||||
|
},
|
||||||
|
Admin(),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
result.Success.Should().BeTrue();
|
||||||
|
overrides.Verify(data => data.ClearOverridesAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
overrides.Verify(data => data.SetOverridesAsync(It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, UserPermissionState>>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
userData.Verify(data => data.UpdateUserAsync(user, It.IsAny<CancellationToken>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
private static TeamMemberService NewService(
|
private static TeamMemberService NewService(
|
||||||
out Mock<UserManager<ApplicationUser>> userManager,
|
out Mock<UserManager<ApplicationUser>> userManager,
|
||||||
out Mock<RoleManager<IdentityRole>> roleManager,
|
out Mock<RoleManager<IdentityRole>> roleManager,
|
||||||
|
|
|
||||||
|
|
@ -168,27 +168,49 @@ public sealed class TeamMemberService : ITeamMemberService
|
||||||
return OperationFailure(roleError);
|
return OperationFailure(roleError);
|
||||||
|
|
||||||
user.FirstName = candidate.Name!.Trim();
|
user.FirstName = candidate.Name!.Trim();
|
||||||
user.Email = email;
|
|
||||||
user.UserName = email;
|
|
||||||
user.NormalizedEmail = email!.ToUpperInvariant();
|
|
||||||
user.NormalizedUserName = email.ToUpperInvariant();
|
|
||||||
user.Contact = candidate.Phone?.Trim();
|
user.Contact = candidate.Phone?.Trim();
|
||||||
user.PhoneNumber = candidate.Phone?.Trim();
|
user.PhoneNumber = candidate.Phone?.Trim();
|
||||||
user.Color = color;
|
user.Color = color;
|
||||||
user.UniqueName = request.IsActive ? ActiveStatus : "Inactive";
|
user.UniqueName = request.IsActive ? ActiveStatus : "Inactive";
|
||||||
user.IsDeleted = !request.IsActive;
|
user.IsDeleted = !request.IsActive;
|
||||||
|
|
||||||
|
if (!string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var emailResult = await _userManager.SetEmailAsync(user, email!);
|
||||||
|
if (!emailResult.Succeeded)
|
||||||
|
return OperationFailure(emailResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
|
||||||
|
|
||||||
|
var userNameResult = await _userManager.SetUserNameAsync(user, email!);
|
||||||
|
if (!userNameResult.Succeeded)
|
||||||
|
return OperationFailure(userNameResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
|
||||||
|
|
||||||
|
var securityStampResult = await _userManager.UpdateSecurityStampAsync(user);
|
||||||
|
if (!securityStampResult.Succeeded)
|
||||||
|
return OperationFailure(securityStampResult.Errors.FirstOrDefault()?.Description ?? "Unable to update email.");
|
||||||
|
}
|
||||||
|
|
||||||
if (roleChanged)
|
if (roleChanged)
|
||||||
{
|
{
|
||||||
if (existingRoles.Count > 0)
|
if (existingRoles.Count > 0)
|
||||||
await _userManager.RemoveFromRolesAsync(user, existingRoles);
|
{
|
||||||
await _userManager.AddToRoleAsync(user, role!);
|
var removeRoleResult = await _userManager.RemoveFromRolesAsync(user, existingRoles);
|
||||||
|
if (!removeRoleResult.Succeeded)
|
||||||
|
return OperationFailure(removeRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var addRoleResult = await _userManager.AddToRoleAsync(user, role!);
|
||||||
|
if (!addRoleResult.Succeeded)
|
||||||
|
return OperationFailure(addRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to update role.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _userDataService.UpdateUserAsync(user, cancellationToken);
|
await _userDataService.UpdateUserAsync(user, cancellationToken);
|
||||||
await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken);
|
await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken);
|
||||||
await _permissionDataService.ClearOverridesAsync(user.Id, cancellationToken);
|
if (roleChanged || request.PermissionOverrides is not null)
|
||||||
await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken);
|
{
|
||||||
|
await _permissionDataService.ClearOverridesAsync(user.Id, cancellationToken);
|
||||||
|
if (request.PermissionOverrides is not null)
|
||||||
|
await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
return await OperationSuccessAsync(user, caller, cancellationToken);
|
return await OperationSuccessAsync(user, caller, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue