using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks; using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces; using Bit.Core.AdminConsole.Utilities.v2.Results; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.HttpResults; using NSubstitute; using Xunit; using ErrorResponseModel = Bit.Core.Models.Api.ErrorResponseModel; namespace Bit.Api.Test.AdminConsole.Controllers; [ControllerCustomize(typeof(OrganizationInviteLinksController))] [SutProviderCustomize] public class OrganizationInviteLinksControllerTests { [Theory, BitAutoData] public async Task Create_WithValidInput_Success( Guid orgId, OrganizationInviteLink inviteLink, SutProvider sutProvider) { inviteLink.OrganizationId = orgId; inviteLink.AllowedDomains = "[\"acme.com\"]"; var model = new CreateOrganizationInviteLinkRequestModel { AllowedDomains = ["acme.com"], EncryptedInviteKey = "encrypted-key", }; sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(new CommandResult(inviteLink)); var result = await sutProvider.Sut.Create(orgId, model); var createdResult = Assert.IsType>(result); Assert.Equal($"organizations/{orgId}/invite-link", createdResult.Location); Assert.NotNull(createdResult.Value); Assert.Equal(inviteLink.Id, createdResult.Value.Id); Assert.Equal(inviteLink.Code, createdResult.Value.Code); Assert.Equal(orgId, createdResult.Value.OrganizationId); await sutProvider.GetDependency() .Received(1) .CreateAsync(Arg.Is(r => r.OrganizationId == orgId && r.EncryptedInviteKey == "encrypted-key")); } [Theory, BitAutoData] public async Task Create_WithExistingLink_Returns409( Guid orgId, SutProvider sutProvider) { var model = new CreateOrganizationInviteLinkRequestModel { AllowedDomains = ["acme.com"], EncryptedInviteKey = "encrypted-key", }; sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(new CommandResult(new InviteLinkAlreadyExists())); var result = await sutProvider.Sut.Create(orgId, model); var jsonResult = Assert.IsType>(result); Assert.Equal(StatusCodes.Status409Conflict, jsonResult.StatusCode); } [Theory, BitAutoData] public async Task Get_WhenLinkExists_ReturnsOkWithModel( Guid orgId, OrganizationInviteLink inviteLink, SutProvider sutProvider) { inviteLink.OrganizationId = orgId; inviteLink.AllowedDomains = "[\"acme.com\"]"; sutProvider.GetDependency() .GetAsync(orgId) .Returns(new CommandResult(inviteLink)); var result = await sutProvider.Sut.Get(orgId); var okResult = Assert.IsType>(result); Assert.NotNull(okResult.Value); Assert.Equal(inviteLink.Id, okResult.Value.Id); Assert.Equal(orgId, okResult.Value.OrganizationId); } [Theory, BitAutoData] public async Task Get_WhenNoLinkExists_ReturnsNotFound( Guid orgId, SutProvider sutProvider) { sutProvider.GetDependency() .GetAsync(orgId) .Returns(new CommandResult(new InviteLinkNotFound())); var result = await sutProvider.Sut.Get(orgId); var notFoundResult = Assert.IsType>(result); Assert.NotNull(notFoundResult.Value); } [Theory, BitAutoData] public async Task Get_WhenInviteLinkNotAvailable_Returns400( Guid orgId, SutProvider sutProvider) { sutProvider.GetDependency() .GetAsync(orgId) .Returns(new CommandResult(new InviteLinkNotAvailable())); var result = await sutProvider.Sut.Get(orgId); var badRequestResult = Assert.IsType>(result); Assert.NotNull(badRequestResult.Value); } [Theory, BitAutoData] public async Task Create_WithValidationError_Returns400( Guid orgId, SutProvider sutProvider) { var model = new CreateOrganizationInviteLinkRequestModel { AllowedDomains = [], EncryptedInviteKey = "encrypted-key", }; sutProvider.GetDependency() .CreateAsync(Arg.Any()) .Returns(new CommandResult(new InviteLinkDomainsRequired())); var result = await sutProvider.Sut.Create(orgId, model); var badRequestResult = Assert.IsType>(result); Assert.NotNull(badRequestResult.Value); } [Theory, BitAutoData] public async Task Update_WithValidInput_ReturnsOk( Guid orgId, OrganizationInviteLink inviteLink, SutProvider sutProvider) { inviteLink.OrganizationId = orgId; inviteLink.AllowedDomains = "[\"acme.com\"]"; var model = new UpdateOrganizationInviteLinkRequestModel { AllowedDomains = ["acme.com"], }; sutProvider.GetDependency() .UpdateAsync(Arg.Any()) .Returns(new CommandResult(inviteLink)); var result = await sutProvider.Sut.Update(orgId, model); var okResult = Assert.IsType>(result); Assert.NotNull(okResult.Value); Assert.Equal(inviteLink.Id, okResult.Value.Id); Assert.Equal(orgId, okResult.Value.OrganizationId); await sutProvider.GetDependency() .Received(1) .UpdateAsync(Arg.Is(r => r.OrganizationId == orgId)); } [Theory, BitAutoData] public async Task Update_WhenNoLinkExists_ReturnsNotFound( Guid orgId, SutProvider sutProvider) { var model = new UpdateOrganizationInviteLinkRequestModel { AllowedDomains = ["acme.com"], }; sutProvider.GetDependency() .UpdateAsync(Arg.Any()) .Returns(new CommandResult(new InviteLinkNotFound())); var result = await sutProvider.Sut.Update(orgId, model); var notFoundResult = Assert.IsType>(result); Assert.NotNull(notFoundResult.Value); } [Theory, BitAutoData] public async Task Update_WithValidationError_Returns400( Guid orgId, SutProvider sutProvider) { var model = new UpdateOrganizationInviteLinkRequestModel { AllowedDomains = [], }; sutProvider.GetDependency() .UpdateAsync(Arg.Any()) .Returns(new CommandResult(new InviteLinkDomainsRequired())); var result = await sutProvider.Sut.Update(orgId, model); var badRequestResult = Assert.IsType>(result); Assert.NotNull(badRequestResult.Value); } [Theory, BitAutoData] public async Task GetStatus_WithValidQuery_Success( GetOrganizationInviteLinkStatusRequestModel model, OrganizationInviteLinkStatus status, SutProvider sutProvider) { sutProvider.GetDependency() .GetStatusAsync(model.Code) .Returns(new CommandResult(status)); var result = await sutProvider.Sut.GetStatus(model); var okResult = Assert.IsType>(result); Assert.Equal(status.OrganizationName, okResult.Value!.OrganizationName); Assert.Equal(status.SeatsAvailable, okResult.Value.SeatsAvailable); } [Theory, BitAutoData] public async Task GetStatus_WithNotFoundError_ReturnsNotFound( GetOrganizationInviteLinkStatusRequestModel model, SutProvider sutProvider) { sutProvider.GetDependency() .GetStatusAsync(model.Code) .Returns(new CommandResult(new InviteLinkNotFound())); var result = await sutProvider.Sut.GetStatus(model); Assert.IsType>(result); } [Theory, BitAutoData] public async Task GetStatus_WithNotAvailableError_ReturnsBadRequest( GetOrganizationInviteLinkStatusRequestModel model, SutProvider sutProvider) { sutProvider.GetDependency() .GetStatusAsync(model.Code) .Returns(new CommandResult(new InviteLinkNotAvailable())); var result = await sutProvider.Sut.GetStatus(model); Assert.IsType>(result); } }