using System.Text.Json; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Mail; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ExceptionExtensions; using NSubstitute.ReturnsExtensions; using Xunit; using Organization = Bit.Core.AdminConsole.Entities.Organization; using OrganizationUser = Bit.Core.Entities.OrganizationUser; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class OrganizationServiceTests { private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); [Theory, PaidOrganizationCustomize, BitAutoData] public async Task OrgImportCreateNewUsers(SutProvider sutProvider, Organization org, List existingUsers, List newUsers) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); org.UseDirectory = true; org.Seats = 10; newUsers.Add(new ImportedOrganizationUser { Email = existingUsers.First().Email, ExternalId = existingUsers.First().ExternalId }); var expectedNewUsersCount = newUsers.Count - 1; existingUsers.First().Type = OrganizationUserType.Owner; sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); var organizationUserRepository = sutProvider.GetDependency(); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); organizationUserRepository.GetManyDetailsByOrganizationAsync(org.Id) .Returns(existingUsers); organizationUserRepository.GetCountByOrganizationIdAsync(org.Id) .Returns(existingUsers.Count); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) .Returns(true); sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .UpsertAsync(default); await sutProvider.GetDependency().Received(1) .UpsertManyAsync(Arg.Is>(users => !users.Any())); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .CreateAsync(default); // Create new users await sutProvider.GetDependency().Received(1) .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync( Arg.Is(info => info.OrgUserTokenPairs.Count() == expectedNewUsersCount && info.IsFreeOrg == (org.PlanType == PlanType.Free) && info.OrganizationName == org.Name)); // Send events await sutProvider.GetDependency().Received(1) .LogOrganizationUserEventsAsync(Arg.Is>(events => events.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .RaiseEventAsync(Arg.Is(referenceEvent => referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id && referenceEvent.Users == expectedNewUsersCount)); } [Theory, PaidOrganizationCustomize, BitAutoData] public async Task OrgImportCreateNewUsersAndMarryExistingUser(SutProvider sutProvider, Organization org, List existingUsers, List newUsers) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); org.UseDirectory = true; org.Seats = newUsers.Count + existingUsers.Count + 1; var reInvitedUser = existingUsers.First(); reInvitedUser.ExternalId = null; newUsers.Add(new ImportedOrganizationUser { Email = reInvitedUser.Email, ExternalId = reInvitedUser.Email, }); var expectedNewUsersCount = newUsers.Count - 1; sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id) .Returns(existingUsers); sutProvider.GetDependency().GetCountByOrganizationIdAsync(org.Id) .Returns(existingUsers.Count); sutProvider.GetDependency().GetByIdAsync(reInvitedUser.Id) .Returns(new OrganizationUser { Id = reInvitedUser.Id }); var organizationUserRepository = sutProvider.GetDependency(); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); var currentContext = sutProvider.GetDependency(); currentContext.ManageUsers(org.Id).Returns(true); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .UpsertAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .CreateAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .CreateAsync(default, default); // Upserted existing user await sutProvider.GetDependency().Received(1) .UpsertManyAsync(Arg.Is>(users => users.Count() == 1)); // Created and invited new users await sutProvider.GetDependency().Received(1) .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == expectedNewUsersCount && info.IsFreeOrg == (org.PlanType == PlanType.Free) && info.OrganizationName == org.Name)); // Sent events await sutProvider.GetDependency().Received(1) .LogOrganizationUserEventsAsync(Arg.Is>(events => events.Where(e => e.Item2 == EventType.OrganizationUser_Invited).Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .RaiseEventAsync(Arg.Is(referenceEvent => referenceEvent.Type == ReferenceEventType.InvitedUsers && referenceEvent.Id == org.Id && referenceEvent.Users == expectedNewUsersCount)); } [Theory, BitAutoData] public async Task SignupClientAsync_Succeeds( OrganizationSignup signup, SutProvider sutProvider) { signup.Plan = PlanType.TeamsMonthly; var plan = StaticStore.GetPlan(signup.Plan); sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(plan); var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup); await sutProvider.GetDependency().Received(1).CreateAsync(Arg.Is(org => org.Id == organization.Id && org.Name == signup.Name && org.Plan == plan.Name && org.PlanType == plan.Type && org.UsePolicies == plan.HasPolicies && org.PublicKey == signup.PublicKey && org.PrivateKey == signup.PrivateKey && org.UseSecretsManager == false)); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(orgApiKey => orgApiKey.OrganizationId == organization.Id)); await sutProvider.GetDependency().Received(1) .UpsertOrganizationAbilityAsync(organization); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(c => c.Name == signup.CollectionName && c.OrganizationId == organization.Id), null, null); await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is( re => re.Type == ReferenceEventType.Signup && re.PlanType == plan.Type)); } [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData] public async Task InviteUsers_NoEmails_Throws(Organization organization, OrganizationUser invitor, OrganizationUserInvite invite, SutProvider sutProvider) { invite.Emails = null; sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); } [Theory] [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData] public async Task InviteUsers_DuplicateEmails_PassesWithoutDuplicates(Organization organization, OrganizationUser invitor, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, OrganizationUserInvite invite, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); invite.Emails = invite.Emails.Append(invite.Emails.First()); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); var organizationUserRepository = sutProvider.GetDependency(); organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) .Returns(new[] { owner }); // Must set guids in order for dictionary of guids to not throw aggregate exceptions SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); } [Theory] [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData] public async Task InviteUsers_SsoOrgWithNullSsoConfig_Passes(Organization organization, OrganizationUser invitor, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, OrganizationUserInvite invite, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); // Org must be able to use SSO to trigger this proper test case as we currently only call to retrieve // an org's SSO config if the org can use SSO organization.UseSso = true; // Return null for sso config sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).ReturnsNull(); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); var organizationUserRepository = sutProvider.GetDependency(); organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) .Returns(new[] { owner }); // Must set guids in order for dictionary of guids to not throw aggregate exceptions SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); } [Theory] [OrganizationInviteCustomize, OrganizationCustomize, BitAutoData] public async Task InviteUsers_SsoOrgWithNeverEnabledRequireSsoPolicy_Passes(Organization organization, SsoConfig ssoConfig, OrganizationUser invitor, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, OrganizationUserInvite invite, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); // Org must be able to use SSO and policies to trigger this test case organization.UseSso = true; organization.UsePolicies = true; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); var organizationUserRepository = sutProvider.GetDependency(); organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) .Returns(new[] { owner }); ssoConfig.Enabled = true; sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig); // Return null policy to mimic new org that's never turned on the require sso policy sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).ReturnsNull(); // Must set guids in order for dictionary of guids to not throw aggregate exceptions SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Admin, InvitorUserType = OrganizationUserType.Owner ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_NoOwner_Throws(Organization organization, OrganizationUser invitor, OrganizationUserInvite invite, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("Organization must have at least one confirmed owner.", exception.Message); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Owner, InvitorUserType = OrganizationUserType.Admin ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { var organizationRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); currentContext.OrganizationAdmin(organization.Id).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("only an owner", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Custom, InvitorUserType = OrganizationUserType.User ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { organization.UseCustomPermissions = true; var organizationRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); currentContext.OrganizationUser(organization.Id).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("your account does not have permission to manage users", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Custom, InvitorUserType = OrganizationUserType.Admin ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsFalse_Throws(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { organization.UseCustomPermissions = false; invite.Permissions = null; invitor.Status = OrganizationUserStatusType.Confirmed; var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner) .Returns(new[] { invitor }); currentContext.OrganizationOwner(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("to enable custom permissions", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Custom, InvitorUserType = OrganizationUserType.Admin ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_WithCustomType_WhenUseCustomPermissionsIsTrue_Passes(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { organization.Seats = 10; organization.UseCustomPermissions = true; invite.Permissions = null; invitor.Status = OrganizationUserStatusType.Confirmed; var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); currentContext.OrganizationOwner(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); } [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.User)] public async Task InviteUsers_WithNonCustomType_WhenUseCustomPermissionsIsFalse_Passes(OrganizationUserType inviteUserType, Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { organization.Seats = 10; organization.UseCustomPermissions = false; invite.Type = inviteUserType; invite.Permissions = null; invitor.Status = OrganizationUserStatusType.Confirmed; var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); currentContext.OrganizationOwner(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); var organizationRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); var organizationUserRepository = sutProvider.GetDependency(); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); currentContext.OrganizationCustom(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("account does not have permission", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Admin, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); var organizationRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); currentContext.OrganizationCustom(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) })); Assert.Contains("can not manage admins", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_NoPermissionsObject_Passes(Organization organization, OrganizationUserInvite invite, OrganizationUser invitor, SutProvider sutProvider) { invite.Permissions = null; invitor.Status = OrganizationUserStatusType.Confirmed; var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); currentContext.OrganizationOwner(organization.Id).Returns(true); currentContext.ManageUsers(organization.Id).Returns(true); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, new (OrganizationUserInvite, string)[] { (invite, null) }); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, SutProvider sutProvider) { // This method is only used to invite 1 user at a time invite.Emails = new[] { invite.Emails.First() }; // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider); var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == 1 && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync(Arg.Any>()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUser_InvitingMoreThanOneUser_Throws(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, SutProvider sutProvider) { var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId)); Assert.Contains("This method can only be used to invite a single user.", exception.Message); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .SendOrganizationInviteEmailsAsync(default); await sutProvider.GetDependency().DidNotReceive() .LogOrganizationUserEventsAsync(Arg.Any>()); await sutProvider.GetDependency().DidNotReceive() .LogOrganizationUserEventsAsync(Arg.Any>()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUser_UserAlreadyInvited_Throws(Organization organization, OrganizationUserInvite invite, string externalId, OrganizationUser invitor, SutProvider sutProvider) { // This method is only used to invite 1 user at a time invite.Emails = new[] { invite.Emails.First() }; // The user has already been invited sutProvider.GetDependency() .SelectKnownEmailsAsync(organization.Id, Arg.Any>(), false) .Returns(new List { invite.Emails.First() }); // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider); var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut .InviteUserAsync(organization.Id, invitor.UserId, systemUser: null, invite, externalId)); Assert.Contains("This user has already been invited", exception.Message); // MailService and EventService are still called, but with no OrgUsers await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => !info.OrgUserTokenPairs.Any() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); await sutProvider.GetDependency().Received(1) .LogOrganizationUserEventsAsync(Arg.Is>(events => !events.Any())); } private void InviteUser_ArrangeCurrentContextPermissions(Organization organization, SutProvider sutProvider) { var currentContext = sutProvider.GetDependency(); currentContext.ManageUsers(organization.Id).Returns(true); currentContext.AccessReports(organization.Id).Returns(true); currentContext.ManageGroups(organization.Id).Returns(true); currentContext.ManagePolicies(organization.Id).Returns(true); currentContext.ManageScim(organization.Id).Returns(true); currentContext.ManageSso(organization.Id).Returns(true); currentContext.AccessEventLogs(organization.Id).Returns(true); currentContext.AccessImportExport(organization.Id).Returns(true); currentContext.EditAnyCollection(organization.Id).Returns(true); currentContext.ManageResetPassword(organization.Id).Returns(true); currentContext.GetOrganization(organization.Id) .Returns(new CurrentContextOrganization() { Permissions = new Permissions { CreateNewCollections = true, DeleteAnyCollection = true } }); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); InviteUser_ArrangeCurrentContextPermissions(organization, sutProvider); var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, systemUser: null, invites); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == invites.SelectMany(i => i.invite.Emails).Count() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync(Arg.Any>()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Custom ), OrganizationCustomize, BitAutoData] public async Task InviteUsers_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser invitor, SutProvider sutProvider) { // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); sutProvider.Create(); invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); var organizationRepository = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var currentContext = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); currentContext.ManageUsers(organization.Id).Returns(true); // Mock tokenable factory to return a token that expires in 5 days sutProvider.GetDependency() .CreateToken(Arg.Any()) .Returns( info => new OrgUserInviteTokenable(info.Arg()) { ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) } ); await sutProvider.Sut.InviteUsersAsync(organization.Id, invitingUserId: null, eventSystemUser, invites); await sutProvider.GetDependency().Received(1) .SendOrganizationInviteEmailsAsync(Arg.Is(info => info.OrgUserTokenPairs.Count() == invites.SelectMany(i => i.invite.Emails).Count() && info.IsFreeOrg == (organization.PlanType == PlanType.Free) && info.OrganizationName == organization.Name)); await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync(Arg.Any>()); } [Theory, BitAutoData, OrganizationCustomize, OrganizationInviteCustomize] public async Task InviteUsers_WithSecretsManager_Passes(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser savingUser, SutProvider sutProvider) { organization.PlanType = PlanType.EnterpriseAnnually; InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider); // Set up some invites to grant access to SM invites.First().invite.AccessSecretsManager = true; var invitedSmUsers = invites.First().invite.Emails.Count(); foreach (var (invite, externalId) in invites.Skip(1)) { invite.AccessSecretsManager = false; } // Assume we need to add seats for all invited SM users sutProvider.GetDependency() .CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers); var organizationUserRepository = sutProvider.GetDependency(); SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); SetupOrgUserRepositoryCreateAsyncMock(organizationUserRepository); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType).Returns(StaticStore.GetPlan(organization.PlanType)); await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites); await sutProvider.GetDependency().Received(1) .UpdateSubscriptionAsync(Arg.Is(update => update.SmSeats == organization.SmSeats + invitedSmUsers && !update.SmServiceAccountsChanged && !update.MaxAutoscaleSmSeatsChanged && !update.MaxAutoscaleSmSeatsChanged)); } [Theory, BitAutoData, OrganizationCustomize, OrganizationInviteCustomize] public async Task InviteUsers_WithSecretsManager_WhenErrorIsThrown_RevertsAutoscaling(Organization organization, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites, OrganizationUser savingUser, SutProvider sutProvider) { var initialSmSeats = organization.SmSeats; InviteUserHelper_ArrangeValidPermissions(organization, savingUser, sutProvider); // Set up some invites to grant access to SM invites.First().invite.AccessSecretsManager = true; var invitedSmUsers = invites.First().invite.Emails.Count(); foreach (var (invite, externalId) in invites.Skip(1)) { invite.AccessSecretsManager = false; } // Assume we need to add seats for all invited SM users sutProvider.GetDependency() .CountNewSmSeatsRequiredAsync(organization.Id, invitedSmUsers).Returns(invitedSmUsers); // Mock SecretsManagerSubscriptionUpdateCommand to actually change the organization's subscription in memory sutProvider.GetDependency() .UpdateSubscriptionAsync(Arg.Any()) .ReturnsForAnyArgs(Task.FromResult(0)).AndDoes(x => organization.SmSeats += invitedSmUsers); // Throw error at the end of the try block sutProvider.GetDependency().RaiseEventAsync(default) .ThrowsForAnyArgs(); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); await Assert.ThrowsAsync(async () => await sutProvider.Sut.InviteUsersAsync(organization.Id, savingUser.Id, systemUser: null, invites)); // OrgUser is reverted // Note: we don't know what their guids are so comparing length is the best we can do var invitedEmails = invites.SelectMany(i => i.invite.Emails); await sutProvider.GetDependency().Received(1).DeleteManyAsync( Arg.Is>(ids => ids.Count() == invitedEmails.Count())); Received.InOrder(() => { // Initial autoscaling sutProvider.GetDependency() .UpdateSubscriptionAsync(Arg.Is(update => update.SmSeats == initialSmSeats + invitedSmUsers && !update.SmServiceAccountsChanged && !update.MaxAutoscaleSmSeatsChanged && !update.MaxAutoscaleSmSeatsChanged)); // Revert autoscaling sutProvider.GetDependency() .UpdateSubscriptionAsync(Arg.Is(update => update.SmSeats == initialSmSeats && !update.SmServiceAccountsChanged && !update.MaxAutoscaleSmSeatsChanged && !update.MaxAutoscaleSmSeatsChanged)); }); } private void InviteUserHelper_ArrangeValidPermissions(Organization organization, OrganizationUser savingUser, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); } [Theory, BitAutoData] public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey, string privateKey, SutProvider sutProvider) { var currentContext = Substitute.For(); currentContext.ManageResetPassword(orgId).Returns(false); await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey)); } [Theory, BitAutoData] public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Throws(Organization org, string publicKey, string privateKey, SutProvider sutProvider) { var currentContext = sutProvider.GetDependency(); currentContext.ManageResetPassword(org.Id).Returns(true); var organizationRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(org.Id).Returns(org); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey)); Assert.Contains("Organization Keys already exist", exception.Message); } [Theory, BitAutoData] public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Success(Organization org, string publicKey, string privateKey, SutProvider sutProvider) { org.PublicKey = null; org.PrivateKey = null; var currentContext = sutProvider.GetDependency(); currentContext.ManageResetPassword(org.Id).Returns(true); var organizationRepository = sutProvider.GetDependency(); organizationRepository.GetByIdAsync(org.Id).Returns(org); await sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey); } [Theory] [PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)] [BitAutoData("Cannot set max seat autoscaling below seat count", 1, 0, 2, 2)] [BitAutoData("Cannot set max seat autoscaling below seat count", 4, -1, 6, 6)] public async Task Enterprise_UpdateMaxSeatAutoscaling_BadInputThrows(string expectedMessage, int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats, Organization organization, SutProvider sutProvider) => await UpdateSubscription_BadInputThrows(expectedMessage, maxAutoscaleSeats, seatAdjustment, currentSeats, currentMaxAutoscaleSeats, organization, sutProvider); [Theory] [FreeOrganizationCustomize] [BitAutoData("Your plan does not allow seat autoscaling", 10, 0, null, null)] public async Task Free_UpdateMaxSeatAutoscaling_BadInputThrows(string expectedMessage, int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats, Organization organization, SutProvider sutProvider) => await UpdateSubscription_BadInputThrows(expectedMessage, maxAutoscaleSeats, seatAdjustment, currentSeats, currentMaxAutoscaleSeats, organization, sutProvider); private async Task UpdateSubscription_BadInputThrows(string expectedMessage, int? maxAutoscaleSeats, int seatAdjustment, int? currentSeats, int? currentMaxAutoscaleSeats, Organization organization, SutProvider sutProvider) { organization.Seats = currentSeats; organization.MaxAutoscaleSeats = currentMaxAutoscaleSeats; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, maxAutoscaleSeats)); Assert.Contains(expectedMessage, exception.Message); } [Theory, BitAutoData] public async Task UpdateSubscription_NoOrganization_Throws(Guid organizationId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns((Organization)null); await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organizationId, 0, null)); } [Theory, SecretsManagerOrganizationCustomize] [BitAutoData("You cannot have more Secrets Manager seats than Password Manager seats.", -1)] public async Task UpdateSubscription_PmSeatAdjustmentLessThanSmSeats_Throws(string expectedMessage, int seatAdjustment, Organization organization, SutProvider sutProvider) { organization.Seats = 100; organization.SmSeats = 100; sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); var actual = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateSubscription(organization.Id, seatAdjustment, null)); Assert.Contains(expectedMessage, actual.Message); } [Theory, PaidOrganizationCustomize] [BitAutoData(0, 100, null, true, "")] [BitAutoData(0, 100, 100, true, "")] [BitAutoData(0, null, 100, true, "")] [BitAutoData(1, 100, null, true, "")] [BitAutoData(1, 100, 100, false, "Seat limit has been reached")] public async Task CanScaleAsync(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats, bool expectedResult, string expectedFailureMessage, Organization organization, SutProvider sutProvider) { organization.Seats = currentSeats; organization.MaxAutoscaleSeats = maxAutoscaleSeats; sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).ReturnsNull(); var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, seatsToAdd); if (expectedFailureMessage == string.Empty) { Assert.Empty(failureMessage); } else { Assert.Contains(expectedFailureMessage, failureMessage); } Assert.Equal(expectedResult, result); } [Theory, PaidOrganizationCustomize, BitAutoData] public async Task CanScaleAsync_FailsOnSelfHosted(Organization organization, SutProvider sutProvider) { sutProvider.GetDependency().SelfHosted.Returns(true); var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10); Assert.False(result); Assert.Contains("Cannot autoscale on self-hosted instance", failureMessage); } [Theory, PaidOrganizationCustomize, BitAutoData] public async Task CanScaleAsync_FailsOnResellerManagedOrganization( Organization organization, SutProvider sutProvider) { var provider = new Provider { Enabled = true, Type = ProviderType.Reseller }; sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(provider); var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10); Assert.False(result); Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage); } private void RestoreRevokeUser_Setup( Organization organization, OrganizationUser? requestingOrganizationUser, OrganizationUser targetOrganizationUser, SutProvider sutProvider) { if (requestingOrganizationUser != null) { requestingOrganizationUser.OrganizationId = organization.Id; } targetOrganizationUser.OrganizationId = organization.Id; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)); sutProvider.GetDependency() .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) .Returns(true); } [Theory, BitAutoData] public async Task RevokeUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser] OrganizationUser organizationUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id); await sutProvider.GetDependency() .Received(1) .RevokeAsync(organizationUser.Id); await sutProvider.GetDependency() .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); await sutProvider.GetDependency() .Received(1) .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); } [Theory, BitAutoData] public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) { RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser); await sutProvider.GetDependency() .Received(1) .RevokeAsync(organizationUser.Id); await sutProvider.GetDependency() .Received(1) .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser); await sutProvider.GetDependency() .Received(1) .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); } [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.TeamsStarter)] [BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ThrowsException_WhenNoSecretsManagerSeats(PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 0, AdditionalServiceAccounts = 5, AdditionalSeats = 2 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("You do not have any Secrets Manager seats!", exception.Message); } [Theory] [BitAutoData(PlanType.Free)] public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingSeats(PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = -1, AdditionalServiceAccounts = 5 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("You can't subtract Secrets Manager seats!", exception.Message); } [Theory] [BitAutoData(PlanType.Free)] public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalServiceAccounts( PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 2, AdditionalServiceAccounts = 5, AdditionalSeats = 3 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("Plan does not allow additional Machine Accounts.", exception.Message); } [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ThrowsException_WhenMoreSeatsThanPasswordManagerSeats(PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 4, AdditionalServiceAccounts = 5, AdditionalSeats = 3 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("You cannot have more Secrets Manager seats than Password Manager seats.", exception.Message); } [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.TeamsStarter)] [BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ThrowsException_WhenSubtractingServiceAccounts( PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 4, AdditionalServiceAccounts = -5, AdditionalSeats = 5 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("You can't subtract Machine Accounts!", exception.Message); } [Theory] [BitAutoData(PlanType.Free)] public void ValidateSecretsManagerPlan_ThrowsException_WhenPlanDoesNotAllowAdditionalUsers( PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 2, AdditionalServiceAccounts = 0, AdditionalSeats = 5 }; var exception = Assert.Throws(() => sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup)); Assert.Contains("Plan does not allow additional users.", exception.Message); } [Theory] [BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.TeamsStarter)] [BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseMonthly)] public void ValidateSecretsManagerPlan_ValidPlan_NoExceptionThrown( PlanType planType, SutProvider sutProvider) { var plan = StaticStore.GetPlan(planType); var signup = new OrganizationUpgrade { UseSecretsManager = true, AdditionalSmSeats = 2, AdditionalServiceAccounts = 0, AdditionalSeats = 4 }; sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Custom, InvitorUserType = OrganizationUserType.Custom ), BitAutoData] public async Task ValidateOrganizationUserUpdatePermissions_WithCustomPermission_WhenSavingUserHasCustomPermission_Passes( CurrentContextOrganization organization, OrganizationUserInvite organizationUserInvite, SutProvider sutProvider) { var invitePermissions = new Permissions { AccessReports = true }; sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); sutProvider.GetDependency().AccessReports(organization.Id).Returns(true); await sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organization.Id, organizationUserInvite.Type.Value, null, invitePermissions); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Owner, InvitorUserType = OrganizationUserType.Admin ), BitAutoData] public async Task ValidateOrganizationUserUpdatePermissions_WithAdminAddingOwner_Throws( Guid organizationId, OrganizationUserInvite organizationUserInvite, SutProvider sutProvider) { var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions)); Assert.Contains("only an owner can configure another owner's account.", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Admin, InvitorUserType = OrganizationUserType.Owner ), BitAutoData] public async Task ValidateOrganizationUserUpdatePermissions_WithoutManageUsersPermission_Throws( Guid organizationId, OrganizationUserInvite organizationUserInvite, SutProvider sutProvider) { var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions)); Assert.Contains("your account does not have permission to manage users.", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Admin, InvitorUserType = OrganizationUserType.Custom ), BitAutoData] public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingAdmin_Throws( Guid organizationId, OrganizationUserInvite organizationUserInvite, SutProvider sutProvider) { sutProvider.GetDependency().ManageUsers(organizationId).Returns(true); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions)); Assert.Contains("custom users can not manage admins or owners.", exception.Message.ToLowerInvariant()); } [Theory] [OrganizationInviteCustomize( InviteeUserType = OrganizationUserType.Custom, InvitorUserType = OrganizationUserType.Custom ), BitAutoData] public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingUser_WithoutPermissions_Throws( Guid organizationId, OrganizationUserInvite organizationUserInvite, SutProvider sutProvider) { var invitePermissions = new Permissions { AccessReports = true }; sutProvider.GetDependency().ManageUsers(organizationId).Returns(true); sutProvider.GetDependency().AccessReports(organizationId).Returns(false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, invitePermissions)); Assert.Contains("custom users can only grant the same custom permissions that they have.", exception.Message.ToLowerInvariant()); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.User)] public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithNotCustomType_IsValid( OrganizationUserType newType, Guid organizationId, SutProvider sutProvider) { await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, newType); } [Theory, BitAutoData] public async Task ValidateOrganizationCustomPermissionsEnabledAsync_NotExistingOrg_ThrowsNotFound( Guid organizationId, SutProvider sutProvider) { await Assert.ThrowsAsync(() => sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, OrganizationUserType.Custom)); } [Theory, BitAutoData] public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithUseCustomPermissionsDisabled_ThrowsBadRequest( Organization organization, SutProvider sutProvider) { organization.UseCustomPermissions = false; sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom)); Assert.Contains("to enable custom permissions", exception.Message.ToLowerInvariant()); } [Theory, BitAutoData] public async Task ValidateOrganizationCustomPermissionsEnabledAsync_WithUseCustomPermissionsEnabled_IsValid( Organization organization, SutProvider sutProvider) { organization.UseCustomPermissions = true; sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom); } // Must set real guids in order for dictionary of guids to not throw aggregate exceptions private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository) { organizationUserRepository.CreateManyAsync(Arg.Any>()).Returns( info => { var orgUsers = info.Arg>(); foreach (var orgUser in orgUsers) { orgUser.Id = Guid.NewGuid(); } return Task.FromResult>(orgUsers.Select(u => u.Id).ToList()); } ); organizationUserRepository.CreateAsync(Arg.Any(), Arg.Any>()).Returns( info => { var orgUser = info.Arg(); orgUser.Id = Guid.NewGuid(); return Task.FromResult(orgUser.Id); } ); } // Must set real guids in order for dictionary of guids to not throw aggregate exceptions private void SetupOrgUserRepositoryCreateAsyncMock(IOrganizationUserRepository organizationUserRepository) { organizationUserRepository.CreateAsync(Arg.Any(), Arg.Any>()).Returns( info => { var orgUser = info.Arg(); orgUser.Id = Guid.NewGuid(); return Task.FromResult(orgUser.Id); } ); } }