using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Xunit; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; public class CollectionRepositoryUpsertDefaultCollectionsTests { /// /// Test that UpsertDefaultCollectionsAsync successfully creates default collections for new users /// [DatabaseTheory, DatabaseData] public async Task UpsertDefaultCollectionsAsync_CreatesDefaultCollections_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository) { // Arrange var user1 = await userRepository.CreateAsync(new User { Name = "Test User 1", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var user2 = await userRepository.CreateAsync(new User { Name = "Test User 2", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, }); var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user2.Id, Status = OrganizationUserStatusType.Confirmed, }); // Act await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser1.Id, orgUser2.Id }, "My Items"); // Assert var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); var defaultCollections = collections.Where(c => c.Type == CollectionType.DefaultUserCollection).ToList(); Assert.Equal(2, defaultCollections.Count); Assert.All(defaultCollections, c => Assert.Equal("My Items", c.Name)); Assert.All(defaultCollections, c => Assert.Equal(organization.Id, c.OrganizationId)); } /// /// Test that calling UpsertDefaultCollectionsAsync multiple times does NOT create duplicates /// [DatabaseTheory, DatabaseData] public async Task UpsertDefaultCollectionsAsync_CalledMultipleTimes_DoesNotCreateDuplicates( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository) { // Arrange var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, }); // Act - Call twice await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser.Id }, "My Items"); // Second call should not create duplicate await Assert.ThrowsAnyAsync(() => collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser.Id }, "My Items")); // Assert - Only one collection should exist var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); var defaultCollections = collections.Where(c => c.Type == CollectionType.DefaultUserCollection).ToList(); Assert.Single(defaultCollections); } /// /// Test that UpsertDefaultCollectionsBulkAsync creates semaphores before collections /// [DatabaseTheory, DatabaseData] public async Task UpsertDefaultCollectionsBulkAsync_CreatesSemaphoresBeforeCollections_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository, DatabaseContext databaseContext) { // Arrange var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, }); // Act await collectionRepository.UpsertDefaultCollectionsBulkAsync( organization.Id, new[] { orgUser.Id }, "My Items"); // Assert - Verify semaphore was created var semaphore = await databaseContext.DefaultCollectionSemaphores .FirstOrDefaultAsync(s => s.OrganizationId == organization.Id && s.OrganizationUserId == orgUser.Id); Assert.NotNull(semaphore); Assert.Equal(organization.Id, semaphore.OrganizationId); Assert.Equal(orgUser.Id, semaphore.OrganizationUserId); // Verify collection was created var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); var defaultCollections = collections.Where(c => c.Type == CollectionType.DefaultUserCollection).ToList(); Assert.Single(defaultCollections); } /// /// Test that deleting an OrganizationUser cascades to DefaultCollectionSemaphore /// [DatabaseTheory, DatabaseData] public async Task DeleteOrganizationUser_CascadesToSemaphore_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository, DatabaseContext databaseContext) { // Arrange var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, }); await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser.Id }, "My Items"); // Verify semaphore exists var semaphoreBefore = await databaseContext.DefaultCollectionSemaphores .FirstOrDefaultAsync(s => s.OrganizationUserId == orgUser.Id); Assert.NotNull(semaphoreBefore); // Act - Delete organization user await organizationUserRepository.DeleteAsync(orgUser); // Assert - Semaphore should be cascade deleted var semaphoreAfter = await databaseContext.DefaultCollectionSemaphores .FirstOrDefaultAsync(s => s.OrganizationUserId == orgUser.Id); Assert.Null(semaphoreAfter); } /// /// Test that deleting an Organization cascades through OrganizationUser to DefaultCollectionSemaphore /// Note: Cascade path is Organization -> OrganizationUser -> DefaultCollectionSemaphore (not direct) /// [DatabaseTheory, DatabaseData] public async Task DeleteOrganization_CascadesThroughOrganizationUser_Success( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository, DatabaseContext databaseContext) { // Arrange var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, }); await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser.Id }, "My Items"); // Verify semaphore exists var semaphoreBefore = await databaseContext.DefaultCollectionSemaphores .FirstOrDefaultAsync(s => s.OrganizationId == organization.Id); Assert.NotNull(semaphoreBefore); // Act - Delete organization (which cascades to OrganizationUser, which cascades to semaphore) await organizationRepository.DeleteAsync(organization); // Assert - Semaphore should be cascade deleted via OrganizationUser var semaphoreAfter = await databaseContext.DefaultCollectionSemaphores .FirstOrDefaultAsync(s => s.OrganizationId == organization.Id); Assert.Null(semaphoreAfter); } /// /// Test that UpsertDefaultCollectionsAsync with empty user list does nothing /// [DatabaseTheory, DatabaseData] public async Task UpsertDefaultCollectionsAsync_WithEmptyList_DoesNothing( IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository) { // Arrange var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); // Act await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, Array.Empty(), "My Items"); // Assert - No collections should be created var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); Assert.Empty(collections); } /// /// Test that UpsertDefaultCollectionsAsync creates CollectionUser entries with correct permissions /// [DatabaseTheory, DatabaseData] public async Task UpsertDefaultCollectionsAsync_CreatesCollectionUsersWithCorrectPermissions( IUserRepository userRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IOrganizationUserRepository organizationUserRepository) { // Arrange var user = await userRepository.CreateAsync(new User { Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", PlanType = PlanType.EnterpriseAnnually, Plan = "Test Plan", BillingEmail = "billing@email.com" }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, }); // Act await collectionRepository.UpsertDefaultCollectionsAsync( organization.Id, new[] { orgUser.Id }, "My Items"); // Assert var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); var defaultCollection = collections.First(c => c.Type == CollectionType.DefaultUserCollection); var collectionUsers = await collectionRepository.GetManyUsersByIdAsync(defaultCollection.Id); var collectionUser = collectionUsers.Single(); Assert.Equal(orgUser.Id, collectionUser.Id); Assert.False(collectionUser.ReadOnly); Assert.False(collectionUser.HidePasswords); Assert.True(collectionUser.Manage); } }