mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 11:53:12 +00:00
fix(team-members): map duplicate-email race to conflict error (SH-325)
CreateAsync checks FindByEmailAsync and then inserts, so two concurrent creates with the same email can both pass the check and hit the unique user-name index. That DbUpdateException was unhandled and surfaced as a 500. Catch it at the CreateAsync call and return the existing "Email is already in use." message, matching the check-then-insert converge pattern already used by SetOverrideCoreAsync.
This commit is contained in:
parent
db8470fbf9
commit
daf3ed9210
2 changed files with 36 additions and 1 deletions
|
|
@ -3,6 +3,7 @@ using Data.SeaHavenIndustries;
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
|
|
@ -95,6 +96,26 @@ public sealed class TeamMemberServiceTests
|
|||
overrides.Verify(data => data.SetOverridesAsync("new-user", It.Is<IReadOnlyDictionary<string, UserPermissionState>>(value => value["deleteSites"] == UserPermissionState.Allow), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_ConcurrentDuplicateEmail_ReturnsAlreadyInUseInsteadOf500()
|
||||
{
|
||||
var service = NewService(out var userManager, out var roleManager, out _, out _);
|
||||
userManager
|
||||
.Setup(manager => manager.FindByEmailAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((ApplicationUser?)null);
|
||||
roleManager.Setup(manager => manager.RoleExistsAsync("Dispatcher")).ReturnsAsync(true);
|
||||
userManager
|
||||
.Setup(manager => manager.CreateAsync(It.IsAny<ApplicationUser>()))
|
||||
.ThrowsAsync(new DbUpdateException("duplicate key", new Exception()));
|
||||
|
||||
var result = await service.CreateAsync(ValidRequest(), Admin(), CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Error.Should().Be("Email is already in use.");
|
||||
userManager.Verify(manager => manager.AddToRoleAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()), Times.Never);
|
||||
userManager.Verify(manager => manager.DeleteAsync(It.IsAny<ApplicationUser>()), Times.Never);
|
||||
}
|
||||
|
||||
private static TeamMemberService NewService(
|
||||
out Mock<UserManager<ApplicationUser>> userManager,
|
||||
out Mock<RoleManager<IdentityRole>> roleManager,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Security.Claims;
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.DTOs;
|
||||
|
|
@ -68,7 +69,20 @@ public sealed class TeamMemberService : ITeamMemberService
|
|||
PendingRegistrationCreatedDate = now
|
||||
};
|
||||
|
||||
var createResult = await _userManager.CreateAsync(user);
|
||||
IdentityResult createResult;
|
||||
try
|
||||
{
|
||||
createResult = await _userManager.CreateAsync(user);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// A concurrent create won the race on the unique user-name index
|
||||
// between the FindByEmailAsync check above and this insert. Surface
|
||||
// the same conflict message instead of letting the database
|
||||
// exception bubble up as a 500.
|
||||
return Failure("Email is already in use.");
|
||||
}
|
||||
|
||||
if (!createResult.Succeeded)
|
||||
return Failure(createResult.Errors.FirstOrDefault()?.Description ?? "Unable to create team member.");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue