shoc-backend/SeaHavenIndustries.Tests/TeamMemberInviteLifecycleTests.cs

139 lines
6.6 KiB
C#

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);
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<TeamMemberOperationOutcomeDTO> UpdateAsync(
TeamMemberInviteTestHost host,
string userId,
string email,
string role,
bool isActive = true) =>
host.InScopeAsync(provider => provider.GetRequiredService<ITeamMemberService>().UpdateAsync(
userId,
new UpdateTeamMemberRequestDTO
{
Name = "Taylor Reed",
Role = role,
Color = "#0D9488",
Email = email,
Phone = "555-0100",
ServiceAreas = role == "dispatcher" ? new[] { "East" } : Array.Empty<string>(),
IsActive = isActive
},
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));
}