using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Xunit; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; public class OrganizationRepositoryTests { [DatabaseTheory, DatabaseData] public async Task GetByClaimedUserDomainAsync_WithVerifiedDomain_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationDomainRepository organizationDomainRepository) { var id = Guid.NewGuid(); var domainName = $"{id}.example.com"; var user1 = await userRepository.CreateAsync(new User { Name = "Test User 1", Email = $"test+{id}@{domainName}", ApiKey = "TEST", SecurityStamp = "stamp", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 1, KdfMemory = 2, KdfParallelism = 3 }); var user2 = await userRepository.CreateAsync(new User { Name = "Test User 2", Email = $"test+{id}@x-{domainName}", // Different domain ApiKey = "TEST", SecurityStamp = "stamp", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 1, KdfMemory = 2, KdfParallelism = 3 }); var user3 = await userRepository.CreateAsync(new User { Name = "Test User 2", Email = $"test+{id}@{domainName}.example.com", // Different domain ApiKey = "TEST", SecurityStamp = "stamp", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 1, KdfMemory = 2, KdfParallelism = 3 }); var organization = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org {id}", BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL Plan = "Test", // TODO: EF does not enforce this being NOT NULL PrivateKey = "privatekey", }); var organizationDomain = new OrganizationDomain { OrganizationId = organization.Id, DomainName = domainName, Txt = "btw+12345", }; organizationDomain.SetVerifiedDate(); organizationDomain.SetNextRunDate(12); organizationDomain.SetJobRunCount(); await organizationDomainRepository.CreateAsync(organizationDomain); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", }); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user2.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", }); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user3.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", }); var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id); var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id); var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id); Assert.NotEmpty(user1Response); Assert.Equal(organization.Id, user1Response.First().Id); Assert.Empty(user2Response); Assert.Empty(user3Response); } [DatabaseTheory, DatabaseData] public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationDomainRepository organizationDomainRepository) { var id = Guid.NewGuid(); var domainName = $"{id}.example.com"; var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{id}@{domainName}", ApiKey = "TEST", SecurityStamp = "stamp", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 1, KdfMemory = 2, KdfParallelism = 3 }); var organization = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org {id}", BillingEmail = user.Email, Plan = "Test", PrivateKey = "privatekey", }); var organizationDomain = new OrganizationDomain { OrganizationId = organization.Id, DomainName = domainName, Txt = "btw+12345", }; organizationDomain.SetNextRunDate(12); organizationDomain.SetJobRunCount(); await organizationDomainRepository.CreateAsync(organizationDomain); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey", }); var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); Assert.Empty(result); } [DatabaseTheory, DatabaseData] public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationDomainRepository organizationDomainRepository) { var id = Guid.NewGuid(); var domainName = $"{id}.example.com"; var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{id}@{domainName}", ApiKey = "TEST", SecurityStamp = "stamp", Kdf = KdfType.PBKDF2_SHA256, KdfIterations = 1, KdfMemory = 2, KdfParallelism = 3 }); var organization1 = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org 1 {id}", BillingEmail = user.Email, Plan = "Test", PrivateKey = "privatekey1", }); var organization2 = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org 2 {id}", BillingEmail = user.Email, Plan = "Test", PrivateKey = "privatekey2", }); var organizationDomain1 = new OrganizationDomain { OrganizationId = organization1.Id, DomainName = domainName, Txt = "btw+12345", }; organizationDomain1.SetNextRunDate(12); organizationDomain1.SetJobRunCount(); organizationDomain1.SetVerifiedDate(); await organizationDomainRepository.CreateAsync(organizationDomain1); var organizationDomain2 = new OrganizationDomain { OrganizationId = organization2.Id, DomainName = domainName, Txt = "btw+67890", }; organizationDomain2.SetNextRunDate(12); organizationDomain2.SetJobRunCount(); organizationDomain2.SetVerifiedDate(); await organizationDomainRepository.CreateAsync(organizationDomain2); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization1.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", }); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization2.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey2", }); var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id); Assert.Equal(2, result.Count); Assert.Contains(result, org => org.Id == organization1.Id); Assert.Contains(result, org => org.Id == organization2.Id); } [DatabaseTheory, DatabaseData] public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty( IOrganizationRepository organizationRepository) { var nonExistentUserId = Guid.NewGuid(); var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId); Assert.Empty(result); } [DatabaseTheory, DatabaseData] public async Task GetManyByIdsAsync_ExistingOrganizations_ReturnsOrganizations(IOrganizationRepository organizationRepository) { var email = "test@email.com"; var organization1 = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org 1", BillingEmail = email, Plan = "Test", PrivateKey = "privatekey1" }); var organization2 = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org 2", BillingEmail = email, Plan = "Test", PrivateKey = "privatekey2" }); var result = await organizationRepository.GetManyByIdsAsync([organization1.Id, organization2.Id]); Assert.Equal(2, result.Count); Assert.Contains(result, org => org.Id == organization1.Id); Assert.Contains(result, org => org.Id == organization2.Id); // Clean up await organizationRepository.DeleteAsync(organization1); await organizationRepository.DeleteAsync(organization2); } [DatabaseTheory, DatabaseData] public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithUsersAndSponsorships_ReturnsCorrectCounts( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository) { // Arrange var organization = await organizationRepository.CreateTestOrganizationAsync(); // Create users in different states var user1 = await userRepository.CreateTestUserAsync("test1"); var user2 = await userRepository.CreateTestUserAsync("test2"); var user3 = await userRepository.CreateTestUserAsync("test3"); // Create organization users in different states await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); // Confirmed state await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); // Invited state // Create a revoked user manually since there's no helper for it await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user3.Id, Status = OrganizationUserStatusType.Revoked, }); // Create sponsorships in different states await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship { SponsoringOrganizationId = organization.Id, IsAdminInitiated = true, ToDelete = false, ValidUntil = null, }); await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship { SponsoringOrganizationId = organization.Id, IsAdminInitiated = true, ToDelete = true, ValidUntil = DateTime.UtcNow.AddDays(1), }); await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship { SponsoringOrganizationId = organization.Id, IsAdminInitiated = true, ToDelete = true, ValidUntil = DateTime.UtcNow.AddDays(-1), // Expired }); await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship { SponsoringOrganizationId = organization.Id, IsAdminInitiated = false, // Not admin initiated ToDelete = false, ValidUntil = null, }); // Act var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); // Assert Assert.Equal(2, result.Users); // Confirmed + Invited users Assert.Equal(2, result.Sponsored); // Two valid sponsorships Assert.Equal(4, result.Total); // Total occupied seats } [DatabaseTheory, DatabaseData] public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithNoUsersOrSponsorships_ReturnsZero( IOrganizationRepository organizationRepository) { // Arrange var organization = await organizationRepository.CreateTestOrganizationAsync(); // Act var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); // Assert Assert.Equal(0, result.Users); Assert.Equal(0, result.Sponsored); Assert.Equal(0, result.Total); } [DatabaseTheory, DatabaseData] public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyRevokedUsers_ReturnsZero( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository) { // Arrange var organization = await organizationRepository.CreateTestOrganizationAsync(); var user = await userRepository.CreateTestUserAsync("test1"); await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Revoked, }); // Act var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); // Assert Assert.Equal(0, result.Users); Assert.Equal(0, result.Sponsored); Assert.Equal(0, result.Total); } [DatabaseTheory, DatabaseData] public async Task GetOccupiedSeatCountByOrganizationIdAsync_WithOnlyExpiredSponsorships_ReturnsZero( IOrganizationRepository organizationRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository) { // Arrange var organization = await organizationRepository.CreateTestOrganizationAsync(); await organizationSponsorshipRepository.CreateAsync(new OrganizationSponsorship { SponsoringOrganizationId = organization.Id, IsAdminInitiated = true, ToDelete = true, ValidUntil = DateTime.UtcNow.AddDays(-1), // Expired }); // Act var result = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); // Assert Assert.Equal(0, result.Users); Assert.Equal(0, result.Sponsored); Assert.Equal(0, result.Total); } [DatabaseTheory, DatabaseData] public async Task IncrementSeatCountAsync_IncrementsSeatCount(IOrganizationRepository organizationRepository) { var organization = await organizationRepository.CreateTestOrganizationAsync(); organization.Seats = 5; await organizationRepository.ReplaceAsync(organization); await organizationRepository.IncrementSeatCountAsync(organization.Id, 3, DateTime.UtcNow); var result = await organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(result); Assert.Equal(8, result.Seats); } [DatabaseData, DatabaseTheory] public async Task IncrementSeatCountAsync_GivenOrganizationHasNotChangedSeatCountBefore_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved( IOrganizationRepository sutRepository) { // Arrange var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); var requestDate = DateTime.UtcNow; // Act await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); // Assert var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); Assert.NotNull(updateResult); Assert.Equal(organization.Id, updateResult.Id); Assert.True(updateResult.SyncSeats); Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); // Annul await sutRepository.DeleteAsync(organization); } [DatabaseData, DatabaseTheory] public async Task IncrementSeatCountAsync_GivenOrganizationHasChangedSeatCountBeforeAndRecordExists_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved( IOrganizationRepository sutRepository) { // Arrange var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); await sutRepository.IncrementSeatCountAsync(organization.Id, 1, DateTime.UtcNow); var requestDate = DateTime.UtcNow; // Act await sutRepository.IncrementSeatCountAsync(organization.Id, 1, DateTime.UtcNow); // Assert var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); Assert.NotNull(updateResult); Assert.Equal(organization.Id, updateResult.Id); Assert.True(updateResult.SyncSeats); Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); // Annul await sutRepository.DeleteAsync(organization); } [DatabaseData, DatabaseTheory] public async Task GetOrganizationsForSubscriptionSyncAsync_GivenOrganizationHasChangedSeatCount_WhenGettingOrgsToUpdate_ThenReturnsOrgSubscriptionUpdate( IOrganizationRepository sutRepository) { // Arrange var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); var requestDate = DateTime.UtcNow; await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); // Act var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); // Assert var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); Assert.NotNull(updateResult); Assert.Equal(organization.Id, updateResult.Id); Assert.True(updateResult.SyncSeats); Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); // Annul await sutRepository.DeleteAsync(organization); } [DatabaseData, DatabaseTheory] public async Task UpdateSuccessfulOrganizationSyncStatusAsync_GivenOrganizationHasChangedSeatCount_WhenUpdatingStatus_ThenSuccessfullyUpdatesOrgSoItDoesntSync( IOrganizationRepository sutRepository) { // Arrange var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); var requestDate = DateTime.UtcNow; var syncDate = DateTime.UtcNow.AddMinutes(1); await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); // Act await sutRepository.UpdateSuccessfulOrganizationSyncStatusAsync([organization.Id], syncDate); // Assert var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); Assert.Null(result.FirstOrDefault(x => x.Id == organization.Id)); // Annul await sutRepository.DeleteAsync(organization); } [DatabaseTheory, DatabaseData] public async Task InitializePendingOrganizationAsync_Success_AllEntitiesUpdated( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository) { var (user, organization, organizationUser) = await CreatePendingOrganizationWithUserAsync( userRepository, organizationRepository, organizationUserRepository, emailVerified: false); var id = Guid.NewGuid(); var publicKey = $"public-key-{id}"; var privateKey = $"private-key-{id}"; var userKey = $"user-key-{id}"; var collectionName = $"Default Collection {id}"; await organizationRepository.InitializePendingOrganizationAsync( organization.Id, publicKey, privateKey, organizationUser.Id, user.Id, userKey, collectionName); var updatedOrg = await organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(updatedOrg); Assert.True(updatedOrg.Enabled); Assert.Equal(OrganizationStatusType.Created, updatedOrg.Status); Assert.Equal(publicKey, updatedOrg.PublicKey); Assert.Equal(privateKey, updatedOrg.PrivateKey); var updatedOrgUser = await organizationUserRepository.GetByIdAsync(organizationUser.Id); Assert.NotNull(updatedOrgUser); Assert.Equal(OrganizationUserStatusType.Confirmed, updatedOrgUser.Status); Assert.Equal(user.Id, updatedOrgUser.UserId); Assert.Equal(userKey, updatedOrgUser.Key); Assert.Null(updatedOrgUser.Email); var updatedUser = await userRepository.GetByIdAsync(user.Id); Assert.NotNull(updatedUser); Assert.True(updatedUser.EmailVerified); var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); Assert.Single(collections); Assert.Equal(collectionName, collections.First().Name); } [DatabaseTheory, DatabaseData] public async Task InitializePendingOrganizationAsync_WithoutCollection_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository) { var (user, organization, organizationUser) = await CreatePendingOrganizationWithUserAsync( userRepository, organizationRepository, organizationUserRepository, emailVerified: true); var id = Guid.NewGuid(); await organizationRepository.InitializePendingOrganizationAsync( organization.Id, $"public-key-{id}", $"private-key-{id}", organizationUser.Id, user.Id, $"user-key-{id}", null); var updatedOrg = await organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(updatedOrg); Assert.True(updatedOrg.Enabled); Assert.Equal(OrganizationStatusType.Created, updatedOrg.Status); var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); Assert.Empty(collections); var updatedUser = await userRepository.GetByIdAsync(user.Id); Assert.True(updatedUser.EmailVerified); } [DatabaseTheory, DatabaseData] public async Task InitializePendingOrganizationAsync_WithInvalidOrganization_ThrowsException( IUserRepository userRepository, IOrganizationRepository organizationRepository) { var user = await userRepository.CreateTestUserAsync(); var nonExistentOrgId = Guid.NewGuid(); var id = Guid.NewGuid(); await Assert.ThrowsAsync(async () => await organizationRepository.InitializePendingOrganizationAsync( nonExistentOrgId, $"public-key-{id}", $"private-key-{id}", Guid.NewGuid(), user.Id, $"user-key-{id}", $"Collection {id}")); } [DatabaseTheory, DatabaseData] public async Task InitializePendingOrganizationAsync_RollbackOnError_NoChangesApplied( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository) { var (user, organization, organizationUser) = await CreatePendingOrganizationWithUserAsync( userRepository, organizationRepository, organizationUserRepository, emailVerified: false); var id = Guid.NewGuid(); var invalidOrgUserId = Guid.NewGuid(); await Assert.ThrowsAsync(async () => await organizationRepository.InitializePendingOrganizationAsync( organization.Id, $"public-key-{id}", $"private-key-{id}", invalidOrgUserId, user.Id, $"user-key-{id}", $"Collection {id}")); var unchangedOrg = await organizationRepository.GetByIdAsync(organization.Id); Assert.NotNull(unchangedOrg); Assert.False(unchangedOrg.Enabled); Assert.Equal(OrganizationStatusType.Pending, unchangedOrg.Status); Assert.Null(unchangedOrg.PublicKey); Assert.Null(unchangedOrg.PrivateKey); var unchangedUser = await userRepository.GetByIdAsync(user.Id); Assert.False(unchangedUser.EmailVerified); } private static async Task<(User user, Organization organization, OrganizationUser organizationUser)> CreatePendingOrganizationWithUserAsync( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, bool emailVerified = false) { var id = Guid.NewGuid(); var user = await userRepository.CreateTestUserAsync(); user.EmailVerified = emailVerified; await userRepository.ReplaceAsync(user); var organization = await organizationRepository.CreateAsync(new Organization { Name = $"Pending Org {id}", BillingEmail = user.Email, Plan = "Teams", Status = OrganizationStatusType.Pending, Enabled = false, PublicKey = null, PrivateKey = null }); var organizationUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, Email = user.Email, Status = OrganizationUserStatusType.Invited, Type = OrganizationUserType.Owner }); return (user, organization, organizationUser); } }