using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Data.SeaHavenIndustries; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; namespace SeaHavenIndustries.Tests; /// What happens to a member's invites when an admin edits or deletes that member. 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() .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() .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); Assert.True(updated.Member!.InviteEmailSent); 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); } [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); } [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); } [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); Assert.Null(updated.Member!.InviteEmailSent); 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 UpdateAsync( TeamMemberInviteTestHost host, string userId, string email, string role, bool isActive = true) => host.InScopeAsync(provider => provider.GetRequiredService().UpdateAsync( userId, new UpdateTeamMemberRequestDTO { Name = "Taylor Reed", Role = role, Color = "#0D9488", Email = email, Phone = "555-0100", ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty(), IsActive = isActive }, TeamMemberInviteTestHost.Admin(), CancellationToken.None)); private static Task CompleteAsync(TeamMemberInviteTestHost host, string token) => host.RegistrationAsync(service => service.CompleteAsync( new CompleteTeamMemberRegistrationRequestDTO { Token = token, Password = Password }, CancellationToken.None)); private static Task ResendInviteAsync(TeamMemberInviteTestHost host, string userId) => host.InScopeAsync(provider => provider.GetRequiredService() .ResendAsync(userId, TeamMemberInviteTestHost.Admin(), CancellationToken.None)); }