diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 64964df13..97e7bf763 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -72,6 +72,33 @@ namespace Bit.Api.Controllers model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); } + [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task ResendSponsorshipOffer(string sponsoringOrgId) + { + // TODO: validate has right to sponsor, send sponsorship email + var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid); + if (sponsoringOrg == null) + { + throw new BadRequestException("Cannot find the requested sponsoring organization."); + } + + var sponsoringOrgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgIdGuid, _currentContext.UserId ?? default); + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); + if (existingOrgSponsorship == null || existingOrgSponsorship.OfferedToEmail == null) + { + throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); + } + + await _organizationsSponsorshipService.SendSponsorshipOfferAsync(sponsoringOrg, existingOrgSponsorship); + } + [HttpPost("redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index 78a47092c..4bb551f4c 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -10,6 +10,7 @@ namespace Bit.Core.Services Task ValidateRedemptionTokenAsync(string encryptedToken); Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName); + Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationSponsorship sponsorship); Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index cac483f2e..84b4381c7 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -91,6 +91,7 @@ namespace Bit.Core.Services { sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); + await SendSponsorshipOfferAsync(sponsoringOrg, sponsorship); await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, sponsoringOrg.Name, RedemptionToken(sponsorship.Id, sponsorshipType)); } @@ -104,6 +105,12 @@ namespace Bit.Core.Services } } + public async Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationSponsorship sponsorship) + { + await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, sponsoringOrg.Name, + RedemptionToken(sponsorship.Id, sponsorship.PlanSponsorshipType.Value)); + } + public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) { if (sponsorship.PlanSponsorshipType == null) diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index a944c56f2..b427bcaf2 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -103,6 +103,100 @@ namespace Bit.Api.Test.Controllers .OfferSponsorshipAsync(default, default, default, default, default); } + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest(Guid sponsoringOrgId, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(sponsoringOrgId.ToString())); + + Assert.Contains("Cannot find the requested sponsoring organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status, + Organization org, OrganizationUser orgUser, + SutProvider sutProvider) + { + orgUser.Status = status; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_NoOfferToEmail_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + sponsorship.OfferedToEmail = null; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + sutProvider.GetDependency().GetBySponsoringOrganizationUserIdAsync(orgUser.Id) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + [Theory] [BitAutoData] public async Task RedeemSponsorship_BadToken_ThrowsBadRequest(string sponsorshipToken, @@ -147,7 +241,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) .Returns((OrganizationSponsorship)null); @@ -169,7 +264,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) .Returns(sponsorship); @@ -193,7 +289,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency() .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); sutProvider.GetDependency() @@ -220,7 +317,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency() .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); sutProvider.GetDependency() @@ -256,21 +354,21 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != orgUser.Id)) .Returns(sponsorship); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns((OrganizationSponsorship)null); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() @@ -280,23 +378,23 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { sponsorship.SponsoredOrganizationId = null; - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != orgUser.Id)) .Returns(sponsorship); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns((OrganizationSponsorship)sponsorship); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() @@ -306,19 +404,19 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns(sponsorship); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("Unable to find the sponsored Organization.", exception.Message); await sutProvider.GetDependency() diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 87914910f..9e595d312 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -92,5 +92,20 @@ namespace Bit.Core.Test.Services await sutProvider.GetDependency().Received(1) .DeleteAsync(createdSponsorship); } + + [Theory] + [BitAutoData] + public async Task SendSponsorshipOfferAsync(Organization org, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.Sut.SendSponsorshipOfferAsync(org, sponsorship); + + await sutProvider.GetDependency().Received(1) + .SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, org.Name, Arg.Any()); + } + + // TODO: test validateSponsorshipAsync + + // TODO: test RemoveSponsorshipAsync } }