2026-09-25 12:45:04 -03:00
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
|
using Data.SeaHavenIndustries;
|
|
|
|
|
using SeaHaven.Services.DTOs;
|
|
|
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
|
|
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
|
|
|
|
|
|
/// <summary>What happens to a member's invites when an admin edits or deletes that member.</summary>
|
|
|
|
|
public sealed class TeamMemberInviteLifecycleTests
|
|
|
|
|
{
|
|
|
|
|
private const string Email = "taylor@example.com";
|
|
|
|
|
private const string CorrectedEmail = "taylor.reed@example.com";
|
|
|
|
|
private const string Password = "Abc1!x";
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task DeletingAnInvitedMember_RemovesTheMemberAndTheirInvites()
|
|
|
|
|
{
|
|
|
|
|
await using var host = await TeamMemberInviteTestHost.CreateAsync();
|
|
|
|
|
var (userId, token) = await host.AddPendingMemberAsync(Email);
|
|
|
|
|
await host.SendCodeAsync(token, Email);
|
|
|
|
|
Assert.True((await ResendInviteAsync(host, userId)).Success);
|
|
|
|
|
Assert.Equal(2, (await host.InvitesAsync(userId)).Count);
|
|
|
|
|
|
|
|
|
|
var deleted = await host.InScopeAsync(provider => provider.GetRequiredService<IUserService>()
|
|
|
|
|
.DeleteUserAsync(userId, TeamMemberInviteTestHost.Admin(), CancellationToken.None));
|
|
|
|
|
|
|
|
|
|
Assert.True(deleted.Success, deleted.Error);
|
|
|
|
|
Assert.Empty(await host.InvitesAsync(userId));
|
|
|
|
|
Assert.False(await host.InScopeAsync(provider => provider.GetRequiredService<ApplicationDbContext>()
|
|
|
|
|
.Users.AnyAsync(user => user.Id == userId)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task ChangingAPendingMembersEmail_RevokesTheOldInviteAndItsConfirmation()
|
|
|
|
|
{
|
|
|
|
|
await using var host = await TeamMemberInviteTestHost.CreateAsync();
|
|
|
|
|
var (userId, oldToken) = await host.AddPendingMemberAsync(Email);
|
|
|
|
|
await host.ConfirmEmailAsync(oldToken, Email);
|
|
|
|
|
|
|
|
|
|
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "admin");
|
|
|
|
|
Assert.True(updated.Success, updated.Error);
|
2026-09-25 12:54:27 -03:00
|
|
|
Assert.True(updated.Member!.InviteEmailSent);
|
2026-09-25 12:45:04 -03:00
|
|
|
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite,
|
|
|
|
|
(await host.RegistrationAsync(service => service.ResolveAsync(oldToken, CancellationToken.None))).Status);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite,
|
|
|
|
|
(await host.RegistrationAsync(service => service.SendCodeAsync(oldToken, CancellationToken.None))).Status);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite, (await CompleteAsync(host, oldToken)).Status);
|
|
|
|
|
|
|
|
|
|
var user = await host.ReloadUserAsync(userId);
|
|
|
|
|
Assert.False(user.EmailConfirmed);
|
|
|
|
|
Assert.True(user.PendingRegistration);
|
|
|
|
|
|
|
|
|
|
// The new address gets its own invite, and it must be confirmed again before finishing.
|
|
|
|
|
var newToken = host.Sent.LatestTokenFor(CorrectedEmail);
|
|
|
|
|
Assert.NotEqual(oldToken, newToken);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.EmailNotConfirmed, (await CompleteAsync(host, newToken)).Status);
|
|
|
|
|
await host.ConfirmEmailAsync(newToken, CorrectedEmail);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.Ok, (await CompleteAsync(host, newToken)).Status);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 12:54:27 -03:00
|
|
|
[Fact]
|
|
|
|
|
public async Task ChangingAPendingMembersEmail_ReportsWhenTheNewInviteCouldNotBeEmailed()
|
|
|
|
|
{
|
|
|
|
|
await using var host = await TeamMemberInviteTestHost.CreateAsync();
|
|
|
|
|
var (userId, _) = await host.AddPendingMemberAsync(Email);
|
|
|
|
|
host.Sent.Succeeds = false;
|
|
|
|
|
|
|
|
|
|
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "dispatcher");
|
|
|
|
|
|
|
|
|
|
Assert.True(updated.Success, updated.Error);
|
|
|
|
|
Assert.Equal(CorrectedEmail, updated.Member!.Email);
|
|
|
|
|
Assert.False(updated.Member.InviteEmailSent);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 13:13:50 -03:00
|
|
|
[Fact]
|
|
|
|
|
public async Task DeactivatingAPendingMemberWhileChangingTheirEmail_RevokesTheInviteWithoutClaimingAnEmail()
|
|
|
|
|
{
|
|
|
|
|
await using var host = await TeamMemberInviteTestHost.CreateAsync();
|
|
|
|
|
var (userId, oldToken) = await host.AddPendingMemberAsync(Email);
|
|
|
|
|
|
|
|
|
|
var updated = await UpdateAsync(host, userId, CorrectedEmail, role: "dispatcher", isActive: false);
|
|
|
|
|
|
|
|
|
|
Assert.True(updated.Success, updated.Error);
|
|
|
|
|
Assert.Null(updated.Member!.InviteEmailSent);
|
|
|
|
|
Assert.DoesNotContain(host.Sent.Messages, message => message.To == CorrectedEmail);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.InvalidInvite,
|
|
|
|
|
(await host.RegistrationAsync(service => service.ResolveAsync(oldToken, CancellationToken.None))).Status);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-25 12:45:04 -03:00
|
|
|
[Fact]
|
|
|
|
|
public async Task EditingAPendingMemberWithoutChangingTheirEmail_KeepsTheirInvite()
|
|
|
|
|
{
|
|
|
|
|
await using var host = await TeamMemberInviteTestHost.CreateAsync();
|
|
|
|
|
var (userId, token) = await host.AddPendingMemberAsync(Email);
|
|
|
|
|
var emailsBefore = host.Sent.Messages.Count;
|
|
|
|
|
|
|
|
|
|
var updated = await UpdateAsync(host, userId, Email.ToUpperInvariant(), role: "dispatcher");
|
|
|
|
|
|
|
|
|
|
Assert.True(updated.Success, updated.Error);
|
2026-09-25 12:54:27 -03:00
|
|
|
Assert.Null(updated.Member!.InviteEmailSent);
|
2026-09-25 12:45:04 -03:00
|
|
|
Assert.Equal(emailsBefore, host.Sent.Messages.Count);
|
|
|
|
|
var invite = Assert.Single(await host.InvitesAsync(userId));
|
|
|
|
|
Assert.Null(invite.RevokedAt);
|
|
|
|
|
Assert.Equal(TeamMemberRegistrationStatus.Ok,
|
|
|
|
|
(await host.RegistrationAsync(service => service.ResolveAsync(token, CancellationToken.None))).Status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Task<TeamMemberOperationOutcomeDTO> UpdateAsync(
|
|
|
|
|
TeamMemberInviteTestHost host,
|
|
|
|
|
string userId,
|
|
|
|
|
string email,
|
2026-09-25 13:13:50 -03:00
|
|
|
string role,
|
|
|
|
|
bool isActive = true) =>
|
2026-09-25 12:45:04 -03:00
|
|
|
host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberService>().UpdateAsync(
|
|
|
|
|
userId,
|
|
|
|
|
new UpdateTeamMemberRequestDTO
|
|
|
|
|
{
|
|
|
|
|
Name = "Taylor Reed",
|
|
|
|
|
Role = role,
|
|
|
|
|
Color = "#0D9488",
|
|
|
|
|
Email = email,
|
|
|
|
|
Phone = "555-0100",
|
2026-09-25 13:13:50 -03:00
|
|
|
ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>(),
|
|
|
|
|
IsActive = isActive
|
2026-09-25 12:45:04 -03:00
|
|
|
},
|
|
|
|
|
TeamMemberInviteTestHost.Admin(),
|
|
|
|
|
CancellationToken.None));
|
|
|
|
|
|
|
|
|
|
private static Task<TeamMemberRegistrationOutcomeDTO> CompleteAsync(TeamMemberInviteTestHost host, string token) =>
|
|
|
|
|
host.RegistrationAsync(service => service.CompleteAsync(
|
|
|
|
|
new CompleteTeamMemberRegistrationRequestDTO { Token = token, Password = Password },
|
|
|
|
|
CancellationToken.None));
|
|
|
|
|
|
|
|
|
|
private static Task<TeamMemberInviteResendOutcomeDTO> ResendInviteAsync(TeamMemberInviteTestHost host, string userId) =>
|
|
|
|
|
host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberInviteService>()
|
|
|
|
|
.ResendAsync(userId, TeamMemberInviteTestHost.Admin(), CancellationToken.None));
|
|
|
|
|
}
|