mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 09:19:34 +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)
|
private static TeamMemberRegistrationStatus StatusFor(IdentityResult result)
|
||||||
{
|
{
|
||||||
// A password already set means the account was registered by another route.
|
// A password already set means the account was registered by another route.
|
||||||
return result.Errors.Any(error => error.Code == nameof(IdentityErrorDescriber.UserAlreadyHasPassword))
|
if (result.Errors.Any(error => error.Code == nameof(IdentityErrorDescriber.UserAlreadyHasPassword)))
|
||||||
? TeamMemberRegistrationStatus.InvalidInvite
|
return TeamMemberRegistrationStatus.InvalidInvite;
|
||||||
: TeamMemberRegistrationStatus.PasswordRejected;
|
|
||||||
|
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;
|
private DateTime NowUtc() => _timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,33 @@ public sealed class TeamMemberInviteRegistrationTests
|
||||||
Assert.False((await host.ReloadUserAsync(userId)).PendingRegistration);
|
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]
|
[Fact]
|
||||||
public async Task Complete_WithoutPhone_KeepsThePhoneTheAdminEntered()
|
public async Task Complete_WithoutPhone_KeepsThePhoneTheAdminEntered()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
||||||
public ManualTimeProvider Time { get; } = new();
|
public ManualTimeProvider Time { get; } = new();
|
||||||
public CapturingLoggerProvider Logged { 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");
|
var connection = new SqliteConnection("Data Source=:memory:;Foreign Keys=True");
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
|
|
@ -48,11 +48,14 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
||||||
await setup.Database.EnsureCreatedAsync();
|
await setup.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
var host = new TeamMemberInviteTestHost(connection);
|
var host = new TeamMemberInviteTestHost(connection);
|
||||||
host._provider = BuildProvider(connection, host);
|
host._provider = BuildProvider(connection, host, configure);
|
||||||
return host;
|
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()
|
var configuration = new ConfigurationBuilder()
|
||||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
|
@ -76,6 +79,7 @@ internal sealed class TeamMemberInviteTestHost : IAsyncDisposable
|
||||||
services.AddSingleton<IEmailSender>(fakes.Sent);
|
services.AddSingleton<IEmailSender>(fakes.Sent);
|
||||||
services.AddDataServices();
|
services.AddDataServices();
|
||||||
services.AddBusinessServices(configuration);
|
services.AddBusinessServices(configuration);
|
||||||
|
configure?.Invoke(services);
|
||||||
return services.BuildServiceProvider();
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue