mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
fix(team-members): report only password-rule failures at finish as a rejected password
This commit is contained in:
parent
e53415de39
commit
afc2330184
3 changed files with 44 additions and 6 deletions
|
|
@ -228,9 +228,16 @@ public sealed partial class TeamMemberRegistrationService : ITeamMemberRegistrat
|
|||
private static TeamMemberRegistrationStatus StatusFor(IdentityResult result)
|
||||
{
|
||||
// A password already set means the account was registered by another route.
|
||||
return result.Errors.Any(error => error.Code == nameof(IdentityErrorDescriber.UserAlreadyHasPassword))
|
||||
? TeamMemberRegistrationStatus.InvalidInvite
|
||||
: TeamMemberRegistrationStatus.PasswordRejected;
|
||||
if (result.Errors.Any(error => error.Code == nameof(IdentityErrorDescriber.UserAlreadyHasPassword)))
|
||||
return TeamMemberRegistrationStatus.InvalidInvite;
|
||||
|
||||
if (IdentityPasswordPolicy.IsPolicyRejection(result))
|
||||
return TeamMemberRegistrationStatus.PasswordRejected;
|
||||
|
||||
// Anything else (a concurrency conflict, a store failure) is not the member's password:
|
||||
// roll back and let the endpoint answer with its generic error. Only codes are recorded.
|
||||
throw new InvalidOperationException(
|
||||
"Identity refused the registration: " + string.Join(", ", result.Errors.Select(error => error.Code)));
|
||||
}
|
||||
|
||||
private DateTime NowUtc() => _timeProvider.GetUtcNow().UtcDateTime;
|
||||
|
|
|
|||
|
|
@ -292,6 +292,33 @@ public sealed class TeamMemberInviteRegistrationTests
|
|||
Assert.False((await host.ReloadUserAsync(userId)).PendingRegistration);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Complete_NonPolicyIdentityFailure_IsNotReportedAsAWeakPasswordAndRollsBack()
|
||||
{
|
||||
await using var host = await TeamMemberInviteTestHost.CreateAsync(services =>
|
||||
services.AddScoped<IPasswordValidator<ApplicationUser>, FailingPasswordValidator>());
|
||||
var (userId, token) = await host.AddPendingMemberAsync(Email);
|
||||
await host.ConfirmEmailAsync(token, Email);
|
||||
|
||||
var failure = await Assert.ThrowsAsync<InvalidOperationException>(() => CompleteAsync(host, token, Password));
|
||||
|
||||
Assert.Contains(FailingPasswordValidator.Code, failure.Message);
|
||||
Assert.DoesNotContain(Password, failure.Message);
|
||||
var pending = await host.ReloadUserAsync(userId);
|
||||
Assert.True(pending.PendingRegistration);
|
||||
Assert.Null(pending.PasswordHash);
|
||||
Assert.Null(Assert.Single(await host.InvitesAsync(userId)).UsedAt);
|
||||
}
|
||||
|
||||
/// <summary>Stands in for any Identity failure that is not the password rule itself.</summary>
|
||||
private sealed class FailingPasswordValidator : IPasswordValidator<ApplicationUser>
|
||||
{
|
||||
public const string Code = "ConcurrencyFailure";
|
||||
|
||||
public Task<IdentityResult> ValidateAsync(UserManager<ApplicationUser> manager, ApplicationUser user, string? password) =>
|
||||
Task.FromResult(IdentityResult.Failed(new IdentityError { Code = Code, Description = "Optimistic concurrency failure." }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Complete_WithoutPhone_KeepsThePhoneTheAdminEntered()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
|||
public ManualTimeProvider Time { get; } = new();
|
||||
public CapturingLoggerProvider Logged { get; } = new();
|
||||
|
||||
public static async Task<TeamMemberInviteTestHost> CreateAsync()
|
||||
public static async Task<TeamMemberInviteTestHost> CreateAsync(Action<IServiceCollection>? configure = null)
|
||||
{
|
||||
var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||||
await connection.OpenAsync();
|
||||
|
|
@ -48,11 +48,14 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
|||
await setup.Database.EnsureCreatedAsync();
|
||||
|
||||
var host = new TeamMemberInviteTestHost(connection);
|
||||
host._provider = BuildProvider(connection, host);
|
||||
host._provider = BuildProvider(connection, host, configure);
|
||||
return host;
|
||||
}
|
||||
|
||||
private static ServiceProvider BuildProvider(SqliteConnection connection, TeamMemberInviteTestHost fakes)
|
||||
private static ServiceProvider BuildProvider(
|
||||
SqliteConnection connection,
|
||||
TeamMemberInviteTestHost fakes,
|
||||
Action<IServiceCollection>? configure)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
|
|
@ -76,6 +79,7 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
|||
services.AddSingleton<IEmailSender>(fakes.Sent);
|
||||
services.AddDataServices();
|
||||
services.AddBusinessServices(configuration);
|
||||
configure?.Invoke(services);
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue