From 47537c4ff833a1efc0514209f08739de7f49cdad Mon Sep 17 00:00:00 2001 From: "D. Ror" Date: Tue, 10 Oct 2023 11:34:17 -0400 Subject: [PATCH] [InviteController] Clean up logic and add tests (#2663) --- .../Controllers/InviteControllerTests.cs | 165 ++++++++++++++++++ Backend/Controllers/InviteController.cs | 98 ++++++----- src/api/models/email-invite-status.ts | 2 +- .../ProjectInvite/ProjectInvite.tsx | 2 +- 4 files changed, 218 insertions(+), 49 deletions(-) create mode 100644 Backend.Tests/Controllers/InviteControllerTests.cs diff --git a/Backend.Tests/Controllers/InviteControllerTests.cs b/Backend.Tests/Controllers/InviteControllerTests.cs new file mode 100644 index 0000000000..1da286f377 --- /dev/null +++ b/Backend.Tests/Controllers/InviteControllerTests.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Backend.Tests.Mocks; +using BackendFramework.Controllers; +using BackendFramework.Interfaces; +using BackendFramework.Models; +using BackendFramework.Services; +using Microsoft.AspNetCore.Mvc; +using NUnit.Framework; + +namespace Backend.Tests.Controllers +{ + public class InviteControllerTests : IDisposable + { + private IProjectRepository _projRepo = null!; + private IUserRepository _userRepo = null!; + private IInviteService _inviteService = null!; + private IPermissionService _permissionService = null!; + private InviteController _inviteController = null!; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _inviteController?.Dispose(); + } + } + + private string _projId = null!; + private string _tokenActive = null!; + private string _tokenExpired = null!; + private const string EmailActive = "active@token.email"; + private const string EmailExpired = "expired@token.email"; + private const string MissingId = "MISSING_ID"; + + [SetUp] + public async Task Setup() + { + _projRepo = new ProjectRepositoryMock(); + _userRepo = new UserRepositoryMock(); + _permissionService = new PermissionServiceMock(); + _inviteService = new InviteService( + _projRepo, _userRepo, _permissionService, new UserRoleRepositoryMock(), new EmailServiceMock()); + _inviteController = new InviteController(_inviteService, _projRepo, _userRepo, _permissionService); + + var tokenPast = new EmailInvite(-1) { Email = EmailExpired }; + _tokenExpired = tokenPast.Token; + var tokenFuture = new EmailInvite(1) { Email = EmailActive }; + _tokenActive = tokenFuture.Token; + _projId = (await _projRepo.Create(new Project + { + Name = "InviteControllerTests", + InviteTokens = new List { tokenPast, tokenFuture } + }))!.Id; + } + + [Test] + public void TestEmailInviteToProject() + { + var data = new EmailInviteData { ProjectId = _projId }; + var result = (ObjectResult)_inviteController.EmailInviteToProject(data).Result; + Assert.That(result.Value, Is.Not.Empty); + } + + [Test] + public void TestEmailInviteToProjectUnauthorized() + { + _inviteController.ControllerContext.HttpContext = PermissionServiceMock.UnauthorizedHttpContext(); + var result = _inviteController.EmailInviteToProject(new EmailInviteData()).Result; + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void TestEmailInviteToProjectNoProject() + { + var data = new EmailInviteData { ProjectId = MissingId }; + var result = _inviteController.EmailInviteToProject(data).Result; + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void TestValidateTokenNoProject() + { + var result = _inviteController.ValidateToken(MissingId, _tokenActive).Result; + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void TestValidateTokenNoTokenNoUser() + { + var result = _inviteController.ValidateToken(_projId, "not-a-token").Result; + Assert.That(result, Is.InstanceOf()); + var value = ((OkObjectResult)result).Value; + Assert.That(value, Is.InstanceOf()); + + var status = (EmailInviteStatus)value!; + Assert.That(status.IsTokenValid, Is.False); + Assert.That(status.IsUserValid, Is.False); + } + + [Test] + public void TestValidateTokenExpiredTokenNoUser() + { + var result = _inviteController.ValidateToken(_projId, _tokenExpired).Result; + Assert.That(result, Is.InstanceOf()); + var value = ((OkObjectResult)result).Value; + Assert.That(value, Is.InstanceOf()); + + var status = (EmailInviteStatus)value!; + Assert.That(status.IsTokenValid, Is.False); + Assert.That(status.IsUserValid, Is.False); + } + + [Test] + public void TestValidateTokenValidTokenNoUser() + { + var result = _inviteController.ValidateToken(_projId, _tokenActive).Result; + Assert.That(result, Is.InstanceOf()); + var value = ((OkObjectResult)result).Value; + Assert.That(value, Is.InstanceOf()); + + var status = (EmailInviteStatus)value!; + Assert.That(status.IsTokenValid, Is.True); + Assert.That(status.IsUserValid, Is.False); + } + + [Test] + public void TestValidateTokenValidTokenUserAlreadyInProject() + { + var roles = new Dictionary { [_projId] = "role-id" }; + _userRepo.Create(new User { Email = EmailActive, ProjectRoles = roles }); + + var result = _inviteController.ValidateToken(_projId, _tokenActive).Result; + Assert.That(result, Is.InstanceOf()); + var value = ((OkObjectResult)result).Value; + Assert.That(value, Is.InstanceOf()); + + var status = (EmailInviteStatus)value!; + Assert.That(status.IsTokenValid, Is.True); + Assert.That(status.IsUserValid, Is.False); + } + + [Test] + public void TestValidateTokenExpiredTokenUserAvailable() + { + _userRepo.Create(new User { Email = EmailExpired }); + + var result = _inviteController.ValidateToken(_projId, _tokenExpired).Result; + Assert.That(result, Is.InstanceOf()); + var value = ((OkObjectResult)result).Value; + Assert.That(value, Is.InstanceOf()); + + var status = (EmailInviteStatus)value!; + Assert.That(status.IsTokenValid, Is.False); + Assert.That(status.IsUserValid, Is.True); + } + } +} diff --git a/Backend/Controllers/InviteController.cs b/Backend/Controllers/InviteController.cs index f4f5b78d1b..5feee3c85a 100644 --- a/Backend/Controllers/InviteController.cs +++ b/Backend/Controllers/InviteController.cs @@ -73,82 +73,86 @@ public async Task ValidateToken(string projectId, string token) var tokenObj = new EmailInvite(); foreach (var tok in project.InviteTokens) { - if (tok.Token == token && DateTime.Now < tok.ExpireTime) + if (tok.Token == token) { - isTokenValid = true; tokenObj = tok; + if (DateTime.Now < tok.ExpireTime) + { + isTokenValid = true; + } break; } } var users = await _userRepo.GetAllUsers(); var currentUser = new User(); - var isUserRegistered = false; + var isUserRegisteredAndNotInProject = false; foreach (var user in users) { if (user.Email == tokenObj.Email) { currentUser = user; - isUserRegistered = true; + if (!user.ProjectRoles.ContainsKey(projectId)) + { + isUserRegisteredAndNotInProject = true; + } break; } } - var status = new EmailInviteStatus(isTokenValid, isUserRegistered); - if (isTokenValid && !isUserRegistered) + var status = new EmailInviteStatus(isTokenValid, isUserRegisteredAndNotInProject); + if (!isTokenValid || !isUserRegisteredAndNotInProject) { return Ok(status); } - if (isTokenValid && isUserRegistered - && !currentUser.ProjectRoles.ContainsKey(projectId) - && await _inviteService.RemoveTokenAndCreateUserRole(project, currentUser, tokenObj)) + if (await _inviteService.RemoveTokenAndCreateUserRole(project, currentUser, tokenObj)) { return Ok(status); } - return Ok(new EmailInviteStatus(false, false)); + return Ok(new EmailInviteStatus(false, true)); } + } - /// - /// This is used in a [FromBody] serializer, so its attributes cannot be set to readonly. - /// - public class EmailInviteData - { - [Required] - public string EmailAddress { get; set; } - [Required] - public string Message { get; set; } - [Required] - public string ProjectId { get; set; } - [Required] - public Role Role { get; set; } - [Required] - public string Domain { get; set; } + /// + /// This is used in a [FromBody] serializer, so its attributes cannot be set to readonly. + /// + public class EmailInviteData + { + [Required] + public string EmailAddress { get; set; } + [Required] + public string Message { get; set; } + [Required] + public string ProjectId { get; set; } + [Required] + public Role Role { get; set; } + [Required] + public string Domain { get; set; } - public EmailInviteData() - { - EmailAddress = ""; - Message = ""; - ProjectId = ""; - Role = Role.Harvester; - Domain = ""; - } + public EmailInviteData() + { + EmailAddress = ""; + Message = ""; + ProjectId = ""; + Role = Role.Harvester; + Domain = ""; } + } - /// - /// This is used in an OpenAPI return value serializer, so its attributes must be defined as properties. - /// - public class EmailInviteStatus - { - [Required] - public bool IsTokenValid { get; set; } - [Required] - public bool IsUserRegistered { get; set; } + /// + /// This is used in an OpenAPI return value serializer, so its attributes must be defined as properties. + /// + public class EmailInviteStatus + { + [Required] + public bool IsTokenValid { get; set; } + [Required] + public bool IsUserValid { get; set; } - public EmailInviteStatus(bool isTokenValid, bool isUserRegistered) - { - IsTokenValid = isTokenValid; - IsUserRegistered = isUserRegistered; - } + public EmailInviteStatus(bool isTokenValid, bool isUserRegistered) + { + IsTokenValid = isTokenValid; + IsUserValid = isUserRegistered; } } } diff --git a/src/api/models/email-invite-status.ts b/src/api/models/email-invite-status.ts index 51bababf90..ed840bbe91 100644 --- a/src/api/models/email-invite-status.ts +++ b/src/api/models/email-invite-status.ts @@ -29,5 +29,5 @@ export interface EmailInviteStatus { * @type {boolean} * @memberof EmailInviteStatus */ - isUserRegistered: boolean; + isUserValid: boolean; } diff --git a/src/components/ProjectInvite/ProjectInvite.tsx b/src/components/ProjectInvite/ProjectInvite.tsx index 9f30bbb738..fcbe7c3d5c 100644 --- a/src/components/ProjectInvite/ProjectInvite.tsx +++ b/src/components/ProjectInvite/ProjectInvite.tsx @@ -24,7 +24,7 @@ export default function ProjectInvite(): ReactElement { const validateLink = useCallback(async (): Promise => { if (project && token) { const status = await backend.validateLink(project, token); - if (status.isTokenValid && status.isUserRegistered) { + if (status.isTokenValid && status.isUserValid) { navigate(Path.Login); return; }