using System.Reflection; using Api.SeaHavenIndustries.Controllers; using FluentAssertions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Moq; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; using System.Security.Claims; using Xunit; namespace Api.SeaHavenIndustries.Tests; public class TeamMemberInviteControllerTests { [Fact] public void RegistrationRequests_CarryNoUserIdentifier() { var requestTypes = new[] { typeof(TeamMemberInviteTokenRequestDTO), typeof(VerifyTeamMemberInviteCodeRequestDTO), typeof(CompleteTeamMemberRegistrationRequestDTO) }; var properties = requestTypes .SelectMany(type => type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) .Select(property => property.Name) .Distinct() .OrderBy(name => name); properties.Should().Equal("Code", "Password", "Phone", "Token"); } [Fact] public void RegistrationEndpoints_AreAnonymousAndNeverCached() { var type = typeof(TeamMemberInviteController); type.GetCustomAttribute().Should().NotBeNull(); var cache = type.GetCustomAttribute(); cache.Should().NotBeNull(); cache!.NoStore.Should().BeTrue(); } [Theory] [InlineData(TeamMemberRegistrationStatus.InvalidInvite)] [InlineData(TeamMemberRegistrationStatus.Ok)] public async Task Complete_WithoutASession_ReturnsTheGenericInviteError(TeamMemberRegistrationStatus status) { var service = new Mock(); service.Setup(x => x.CompleteAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(new TeamMemberRegistrationOutcomeDTO { Status = status }); var controller = new TeamMemberInviteController(service.Object) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; var result = await controller.Complete(new CompleteTeamMemberRegistrationRequestDTO(), CancellationToken.None); var failure = result.Should().BeOfType().Subject; failure.StatusCode.Should().Be(StatusCodes.Status400BadRequest); failure.Value.Should().BeEquivalentTo(new { code = "invalid_invite", message = TeamMemberInviteController.InvalidInviteMessage }); } [Fact] public void ResendInvite_RequiresAuthenticatedCaller() { typeof(TeamMemberController).GetCustomAttribute().Should().NotBeNull(); typeof(TeamMemberController).GetMethod(nameof(TeamMemberController.ResendInvite))! .GetCustomAttribute().Should().BeNull(); } [Fact] public async Task ResendInvite_Success_ReturnsInviteSent() { var invites = new Mock(); invites.Setup(x => x.ResendAsync("user-1", It.IsAny(), It.IsAny())) .ReturnsAsync(new TeamMemberInviteResendOutcomeDTO { Success = true }); var result = await NewTeamMemberController(invites).ResendInvite("user-1", CancellationToken.None); result.Should().BeOfType().Which.Value.Should().BeEquivalentTo(new { message = "Invite sent" }); } [Theory] [InlineData("Forbidden", typeof(ForbidResult))] [InlineData("Team member not found.", typeof(NotFoundObjectResult))] [InlineData("Only pending team members can be re-invited.", typeof(BadRequestObjectResult))] [InlineData("The invite could not be emailed. Try again.", typeof(BadRequestObjectResult))] public async Task ResendInvite_Failure_MapsToHttpResult(string error, Type expected) { var invites = new Mock(); invites.Setup(x => x.ResendAsync("user-1", It.IsAny(), It.IsAny())) .ReturnsAsync(new TeamMemberInviteResendOutcomeDTO { Success = false, Error = error }); var result = await NewTeamMemberController(invites).ResendInvite("user-1", CancellationToken.None); result.Should().BeOfType(expected); } private static TeamMemberController NewTeamMemberController(Mock invites) { return new TeamMemberController(Mock.Of(), invites.Object) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "Admin") }, "Test")) } } }; } }