diff --git a/src/Core/AdminConsole/Collections/DuplicateDefaultCollectionException.cs b/src/Core/AdminConsole/Collections/DuplicateDefaultCollectionException.cs
deleted file mode 100644
index abe1cdedb0..0000000000
--- a/src/Core/AdminConsole/Collections/DuplicateDefaultCollectionException.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Bit.Core.AdminConsole.Collections;
-
-public class DuplicateDefaultCollectionException()
- : Exception("A My Items collection already exists for one or more of the specified organization members.");
-
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Collections/CollectionUtils.cs b/src/Core/AdminConsole/OrganizationFeatures/Collections/CollectionUtils.cs
index fddfd37d8e..6b2da70d3e 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Collections/CollectionUtils.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Collections/CollectionUtils.cs
@@ -7,21 +7,19 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Collections;
public static class CollectionUtils
{
///
- /// Arranges semaphore, Collection and CollectionUser objects to create default user collections.
+ /// Arranges Collection and CollectionUser objects to create default user collections.
///
/// The organization ID.
/// The IDs for organization users who need default collections.
/// The encrypted string to use as the default collection name.
- /// A tuple containing the semaphores, collections, and collection users.
- public static (IEnumerable semaphores,
- IEnumerable collections,
+ /// A tuple containing the collections and collection users.
+ public static (IEnumerable collections,
IEnumerable collectionUsers)
BuildDefaultUserCollections(Guid organizationId, IEnumerable organizationUserIds,
string defaultCollectionName)
{
var now = DateTime.UtcNow;
- var semaphores = new List();
var collectionUsers = new List();
var collections = new List();
@@ -29,12 +27,6 @@ public static class CollectionUtils
{
var collectionId = CoreHelpers.GenerateComb();
- semaphores.Add(new DefaultCollectionSemaphore
- {
- OrganizationUserId = orgUserId,
- CreationDate = now
- });
-
collections.Add(new Collection
{
Id = collectionId,
@@ -57,6 +49,6 @@ public static class CollectionUtils
});
}
- return (semaphores, collections, collectionUsers);
+ return (collections, collectionUsers);
}
}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
index 22d9fd20dd..6f763b18be 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs
@@ -72,18 +72,9 @@ public class OrganizationDataOwnershipPolicyValidator(
return;
}
- // Filter out users who already have default collections
- var existingSemaphores = await collectionRepository.GetDefaultCollectionSemaphoresAsync(userOrgIds);
- var usersNeedingDefaultCollections = userOrgIds.Except(existingSemaphores).ToList();
-
- if (!usersNeedingDefaultCollections.Any())
- {
- return;
- }
-
await collectionRepository.CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId,
- usersNeedingDefaultCollections,
+ userOrgIds,
defaultCollectionName);
}
}
diff --git a/src/Core/Entities/DefaultCollectionSemaphore.cs b/src/Core/Entities/DefaultCollectionSemaphore.cs
deleted file mode 100644
index ea2568e671..0000000000
--- a/src/Core/Entities/DefaultCollectionSemaphore.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Bit.Core.Entities;
-
-public class DefaultCollectionSemaphore
-{
- public Guid OrganizationUserId { get; set; }
- public DateTime CreationDate { get; set; } = DateTime.UtcNow;
-}
diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs
index 91232db058..3f3b71d2d5 100644
--- a/src/Core/Repositories/ICollectionRepository.cs
+++ b/src/Core/Repositories/ICollectionRepository.cs
@@ -65,7 +65,7 @@ public interface ICollectionRepository : IRepository
///
/// Creates default user collections for the specified organization users.
- /// Throws an exception if any user already has a default collection for the organization.
+ /// Filters internally to only create collections for users who don't already have one.
///
/// The Organization ID.
/// The Organization User IDs to create default collections for.
@@ -75,27 +75,11 @@ public interface ICollectionRepository : IRepository
///
/// Creates default user collections for the specified organization users using bulk insert operations.
/// Use this if you need to create collections for > ~1k users.
- /// Throws an exception if any user already has a default collection for the organization.
+ /// Filters internally to only create collections for users who don't already have one.
///
/// The Organization ID.
/// The Organization User IDs to create default collections for.
/// The encrypted string to use as the default collection name.
- ///
- /// If any of the OrganizationUsers may already have default collections, the caller should first filter out these
- /// users using GetDefaultCollectionSemaphoresAsync before calling this method.
- ///
Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName);
- ///
- /// Gets default collection semaphores for the given organizationUserIds.
- /// If an organizationUserId is missing from the result set, they do not have a semaphore set.
- ///
- /// The organization User IDs to check semaphores for.
- /// Collection of organization user IDs that have default collection semaphores.
- ///
- /// The semaphore table is used to ensure that an organizationUser can only have 1 default collection.
- /// (That is, a user may only have 1 default collection per organization.)
- /// If a semaphore is returned, that user already has a default collection for that organization.
- ///
- Task> GetDefaultCollectionSemaphoresAsync(IEnumerable organizationUserIds);
}
diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs
index 1416945936..a78a699b10 100644
--- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs
+++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs
@@ -1,7 +1,6 @@
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
-using Bit.Core.AdminConsole.Collections;
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
@@ -392,11 +391,6 @@ public class CollectionRepository : Repository, ICollectionRep
await transaction.CommitAsync();
}
- catch (Exception ex) when (DatabaseExceptionHelpers.IsDuplicateKeyException(ex))
- {
- await transaction.RollbackAsync();
- throw new DuplicateDefaultCollectionException();
- }
catch
{
await transaction.RollbackAsync();
@@ -406,97 +400,8 @@ public class CollectionRepository : Repository, ICollectionRep
public async Task CreateDefaultCollectionsBulkAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName)
{
- organizationUserIds = organizationUserIds.ToList();
- if (!organizationUserIds.Any())
- {
- return;
- }
-
- var (semaphores, collections, collectionUsers) =
- CollectionUtils.BuildDefaultUserCollections(organizationId, organizationUserIds, defaultCollectionName);
-
- await using var connection = new SqlConnection(ConnectionString);
- connection.Open();
- await using var transaction = connection.BeginTransaction();
-
- try
- {
- // CRITICAL: Insert semaphore entries BEFORE collections
- // Database will throw on duplicate primary key (OrganizationUserId)
- await BulkInsertDefaultCollectionSemaphoresAsync(connection, transaction, semaphores);
- await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections);
- await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers);
-
- await transaction.CommitAsync();
- }
- catch (Exception ex) when (DatabaseExceptionHelpers.IsDuplicateKeyException(ex))
- {
- await transaction.RollbackAsync();
- throw new DuplicateDefaultCollectionException();
- }
- catch
- {
- await transaction.RollbackAsync();
- throw;
- }
- }
-
- public async Task> GetDefaultCollectionSemaphoresAsync(IEnumerable organizationUserIds)
- {
- await using var connection = new SqlConnection(ConnectionString);
-
- var results = await connection.QueryAsync(
- "[dbo].[DefaultCollectionSemaphore_ReadByOrganizationUserIds]",
- new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() },
- commandType: CommandType.StoredProcedure);
-
- return results.ToHashSet();
- }
-
- private async Task BulkInsertDefaultCollectionSemaphoresAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable semaphores)
- {
- semaphores = semaphores.ToList();
- if (!semaphores.Any())
- {
- return;
- }
-
- // Sort by composite key to reduce deadlocks
- var sortedSemaphores = semaphores
- .OrderBy(s => s.OrganizationUserId)
- .ToList();
-
- using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity | SqlBulkCopyOptions.CheckConstraints, transaction);
- bulkCopy.DestinationTableName = "[dbo].[DefaultCollectionSemaphore]";
- bulkCopy.BatchSize = 500;
- bulkCopy.BulkCopyTimeout = 120;
- bulkCopy.EnableStreaming = true;
-
- var dataTable = new DataTable("DefaultCollectionSemaphoreDataTable");
-
- var organizationUserIdColumn = new DataColumn(nameof(DefaultCollectionSemaphore.OrganizationUserId), typeof(Guid));
- dataTable.Columns.Add(organizationUserIdColumn);
- var creationDateColumn = new DataColumn(nameof(DefaultCollectionSemaphore.CreationDate), typeof(DateTime));
- dataTable.Columns.Add(creationDateColumn);
-
- foreach (DataColumn col in dataTable.Columns)
- {
- bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
- }
-
- var keys = new DataColumn[1];
- keys[0] = organizationUserIdColumn;
- dataTable.PrimaryKey = keys;
-
- foreach (var semaphore in sortedSemaphores)
- {
- var row = dataTable.NewRow();
- row[organizationUserIdColumn] = semaphore.OrganizationUserId;
- row[creationDateColumn] = semaphore.CreationDate;
- dataTable.Rows.Add(row);
- }
-
- await bulkCopy.WriteToServerAsync(dataTable);
+ // Use the stored procedure approach which handles filtering internally
+ await CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultCollectionName);
}
public class CollectionWithGroupsAndUsers : Collection
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/DefaultCollectionSemaphoreEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/DefaultCollectionSemaphoreEntityTypeConfiguration.cs
deleted file mode 100644
index 3892941111..0000000000
--- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/DefaultCollectionSemaphoreEntityTypeConfiguration.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Metadata.Builders;
-
-namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
-
-public class DefaultCollectionSemaphoreEntityTypeConfiguration : IEntityTypeConfiguration
-{
- public void Configure(EntityTypeBuilder builder)
- {
- builder
- .HasKey(dcs => new { dcs.OrganizationUserId });
-
- // OrganizationUser FK cascades deletions to ensure automatic cleanup
- builder
- .HasOne(dcs => dcs.OrganizationUser)
- .WithMany()
- .HasForeignKey(dcs => dcs.OrganizationUserId)
- .OnDelete(DeleteBehavior.Cascade);
-
- builder.ToTable(nameof(DefaultCollectionSemaphore));
- }
-}
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/DefaultCollectionSemaphore.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/DefaultCollectionSemaphore.cs
deleted file mode 100644
index 6d51bb9f21..0000000000
--- a/src/Infrastructure.EntityFramework/AdminConsole/Models/DefaultCollectionSemaphore.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using AutoMapper;
-using Bit.Infrastructure.EntityFramework.Models;
-
-namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
-
-public class DefaultCollectionSemaphore : Core.Entities.DefaultCollectionSemaphore
-{
- public virtual OrganizationUser? OrganizationUser { get; set; }
-}
-
-public class DefaultCollectionSemaphoreMapperProfile : Profile
-{
- public DefaultCollectionSemaphoreMapperProfile()
- {
- CreateMap()
- .ForMember(dcs => dcs.OrganizationUser, opt => opt.Ignore())
- .ReverseMap();
- }
-}
diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs
index 65bdb68312..6211f71873 100644
--- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs
@@ -1,5 +1,4 @@
using AutoMapper;
-using Bit.Core.AdminConsole.Collections;
using Bit.Core.AdminConsole.OrganizationFeatures.Collections;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@@ -804,28 +803,38 @@ public class CollectionRepository : Repository organizationUserIdsHashSet.Contains(cu.OrganizationUserId))
+ .Where(cu => cu.Collection.Type == CollectionType.DefaultUserCollection)
+ .Where(cu => cu.Collection.OrganizationId == organizationId)
+ .Select(cu => cu.OrganizationUserId)
+ .ToListAsync();
+
+ // Filter to only users who need collections
+ var filteredOrgUserIds = organizationUserIds.Except(existingOrgUserIds).ToList();
+
+ if (!filteredOrgUserIds.Any())
+ {
+ return;
+ }
+
+ var (collections, collectionUsers) =
+ CollectionUtils.BuildDefaultUserCollections(organizationId, filteredOrgUserIds, defaultCollectionName);
+
await using var transaction = await dbContext.Database.BeginTransactionAsync();
try
{
- // CRITICAL: Insert semaphore entries BEFORE collections
- // Database will throw on duplicate primary key (OrganizationUserId)
- await dbContext.BulkCopyAsync(Mapper.Map>(semaphores));
await dbContext.BulkCopyAsync(Mapper.Map>(collections));
await dbContext.BulkCopyAsync(Mapper.Map>(collectionUsers));
await transaction.CommitAsync();
}
- catch (Exception ex) when (DatabaseExceptionHelpers.IsDuplicateKeyException(ex))
- {
- await transaction.RollbackAsync();
- throw new DuplicateDefaultCollectionException();
- }
catch
{
await transaction.RollbackAsync();
@@ -839,18 +848,4 @@ public class CollectionRepository : Repository> GetDefaultCollectionSemaphoresAsync(IEnumerable organizationUserIds)
- {
- var organizationUserIdsHashSet = organizationUserIds.ToHashSet();
-
- using var scope = ServiceScopeFactory.CreateScope();
- var dbContext = GetDatabaseContext(scope);
-
- var result = await dbContext.DefaultCollectionSemaphores
- .Where(s => organizationUserIdsHashSet.Contains(s.OrganizationUserId))
- .Select(s => s.OrganizationUserId)
- .ToListAsync();
-
- return result.ToHashSet();
- }
}
diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
index f48bbc6c24..3ddcad55c3 100644
--- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs
@@ -43,7 +43,6 @@ public class DatabaseContext : DbContext
public DbSet CollectionCiphers { get; set; }
public DbSet CollectionGroups { get; set; }
public DbSet CollectionUsers { get; set; }
- public DbSet DefaultCollectionSemaphores { get; set; }
public DbSet Devices { get; set; }
public DbSet EmergencyAccesses { get; set; }
public DbSet Events { get; set; }
diff --git a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateDefaultCollections.sql b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateDefaultCollections.sql
index f751b8776e..b110a7566d 100644
--- a/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateDefaultCollections.sql
+++ b/src/Sql/dbo/AdminConsole/Stored Procedures/Collection_CreateDefaultCollections.sql
@@ -1,6 +1,6 @@
-- Creates default user collections for organization users
--- Uses semaphore table to prevent duplicate default collections at database level
--- NOTE: this MUST be executed in a single transaction to obtain semaphore protection
+-- Filters out existing default collections at database level
+-- NOTE: this MUST be executed in a single transaction to ensure consistency
CREATE PROCEDURE [dbo].[Collection_CreateDefaultCollections]
@OrganizationId UNIQUEIDENTIFIER,
@DefaultCollectionName VARCHAR(MAX),
@@ -11,20 +11,20 @@ BEGIN
DECLARE @Now DATETIME2(7) = GETUTCDATE()
- -- Insert semaphore entries first to obtain the "lock"
- -- If this fails due to duplicate key, the entire transaction will be rolled back
- INSERT INTO [dbo].[DefaultCollectionSemaphore]
- (
- [OrganizationUserId],
- [CreationDate]
- )
- SELECT
- ids.[Id1], -- OrganizationUserId
- @Now
- FROM
- @OrganizationUserCollectionIds ids;
+ -- Filter to only users who don't have default collections
+ SELECT ids.Id1, ids.Id2
+ INTO #FilteredIds
+ FROM @OrganizationUserCollectionIds ids
+ WHERE NOT EXISTS (
+ SELECT 1
+ FROM [dbo].[CollectionUser] cu
+ INNER JOIN [dbo].[Collection] c ON c.Id = cu.CollectionId
+ WHERE c.OrganizationId = @OrganizationId
+ AND c.[Type] = 1 -- CollectionType.DefaultUserCollection
+ AND cu.OrganizationUserId = ids.Id1
+ );
- -- Insert collections for users who obtained semaphore entries
+ -- Insert collections only for users who don't have default collections yet
INSERT INTO [dbo].[Collection]
(
[Id],
@@ -37,7 +37,7 @@ BEGIN
[DefaultUserCollectionEmail]
)
SELECT
- ids.[Id2], -- CollectionId
+ ids.Id2, -- CollectionId
@OrganizationId,
@DefaultCollectionName,
@Now,
@@ -46,7 +46,7 @@ BEGIN
NULL,
NULL
FROM
- @OrganizationUserCollectionIds ids;
+ #FilteredIds ids;
-- Insert collection user mappings
INSERT INTO [dbo].[CollectionUser]
@@ -58,11 +58,13 @@ BEGIN
[Manage]
)
SELECT
- ids.[Id2], -- CollectionId
- ids.[Id1], -- OrganizationUserId
+ ids.Id2, -- CollectionId
+ ids.Id1, -- OrganizationUserId
0, -- ReadOnly = false
0, -- HidePasswords = false
1 -- Manage = true
FROM
- @OrganizationUserCollectionIds ids;
+ #FilteredIds ids;
+
+ DROP TABLE #FilteredIds;
END
diff --git a/src/Sql/dbo/AdminConsole/Tables/DefaultCollectionSemaphore.sql b/src/Sql/dbo/AdminConsole/Tables/DefaultCollectionSemaphore.sql
deleted file mode 100644
index 6161781c8c..0000000000
--- a/src/Sql/dbo/AdminConsole/Tables/DefaultCollectionSemaphore.sql
+++ /dev/null
@@ -1,11 +0,0 @@
--- Semaphore table to prevent duplicate default collections per organization user
--- Cascade behavior: Organization -> OrganizationUser (CASCADE) -> DefaultCollectionSemaphore (CASCADE)
--- OrganizationId FK has NO ACTION to avoid competing cascade paths
-CREATE TABLE [dbo].[DefaultCollectionSemaphore]
-(
- [OrganizationUserId] UNIQUEIDENTIFIER NOT NULL,
- [CreationDate] DATETIME2 (7) NOT NULL,
- CONSTRAINT [PK_DefaultCollectionSemaphore] PRIMARY KEY CLUSTERED ([OrganizationUserId] ASC),
- CONSTRAINT [FK_DefaultCollectionSemaphore_OrganizationUser] FOREIGN KEY ([OrganizationUserId])
- REFERENCES [dbo].[OrganizationUser] ([Id]) ON DELETE CASCADE
-);
diff --git a/src/Sql/dbo/Stored Procedures/DefaultCollectionSemaphore_ReadByOrganizationUserIds.sql b/src/Sql/dbo/Stored Procedures/DefaultCollectionSemaphore_ReadByOrganizationUserIds.sql
deleted file mode 100644
index b1bfae98b6..0000000000
--- a/src/Sql/dbo/Stored Procedures/DefaultCollectionSemaphore_ReadByOrganizationUserIds.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-CREATE PROCEDURE [dbo].[DefaultCollectionSemaphore_ReadByOrganizationUserIds]
- @OrganizationUserIds AS [dbo].[GuidIdArray] READONLY
-AS
-BEGIN
- SET NOCOUNT ON
-
- SELECT
- [OrganizationUserId]
- FROM
- [dbo].[DefaultCollectionSemaphore] DCS
- INNER JOIN
- @OrganizationUserIds OU ON [OU].[Id] = [DCS].[OrganizationUserId]
-END
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
index b61888759a..5d6bac0ead 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs
@@ -198,22 +198,13 @@ public class OrganizationDataOwnershipPolicyValidatorTests
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For();
- // Mock GetDefaultCollectionSemaphoresAsync to return empty set (no existing collections)
- collectionRepository
- .GetDefaultCollectionSemaphoresAsync(Arg.Any>())
- .Returns(new HashSet());
-
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
- // Assert
- await collectionRepository
- .Received(1)
- .GetDefaultCollectionSemaphoresAsync(Arg.Is>(ids => ids.Count() == 3));
-
+ // Assert - Should call with all user IDs (repository does internal filtering)
await collectionRepository
.Received(1)
.CreateDefaultCollectionsBulkAsync(
@@ -224,7 +215,7 @@ public class OrganizationDataOwnershipPolicyValidatorTests
[Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
- public async Task ExecuteSideEffectsAsync_FiltersOutUsersWithExistingCollections(
+ public async Task ExecuteSideEffectsAsync_PassesAllUsers_RepositoryFiltersInternally(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
@@ -241,34 +232,24 @@ public class OrganizationDataOwnershipPolicyValidatorTests
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For();
- // Mock GetDefaultCollectionSemaphoresAsync to return one existing user
- var existingUserId = orgPolicyDetailsList[0].OrganizationUserId;
- collectionRepository
- .GetDefaultCollectionSemaphoresAsync(Arg.Any>())
- .Returns([existingUserId]);
-
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
- // Assert - Should filter out the existing user
- await collectionRepository
- .Received(1)
- .GetDefaultCollectionSemaphoresAsync(Arg.Is>(ids => ids.Count() == 3));
-
+ // Assert - Should pass all user IDs (repository does internal filtering)
await collectionRepository
.Received(1)
.CreateDefaultCollectionsBulkAsync(
policyUpdate.OrganizationId,
- Arg.Is>(ids => ids.Count() == 2 && !ids.Contains(existingUserId)),
+ Arg.Is>(ids => ids.Count() == 3),
_defaultUserCollectionName);
}
[Theory]
[BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))]
- public async Task ExecuteSideEffectsAsync_DoesNotCallRepository_WhenAllUsersHaveExistingCollections(
+ public async Task ExecuteSideEffectsAsync_CallsRepositoryWithAllUsers_EvenIfAllHaveCollections(
Policy postUpdatedPolicy,
Policy? previousPolicyState,
[PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate,
@@ -285,26 +266,19 @@ public class OrganizationDataOwnershipPolicyValidatorTests
var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList);
var collectionRepository = Substitute.For();
- // Mock GetDefaultCollectionSemaphoresAsync to return all users
- var allUserIds = orgPolicyDetailsList.Select(p => p.OrganizationUserId).ToHashSet();
- collectionRepository
- .GetDefaultCollectionSemaphoresAsync(Arg.Any>())
- .Returns(allUserIds);
-
var sut = ArrangeSut(factory, policyRepository, collectionRepository);
var policyRequest = new SavePolicyModel(policyUpdate, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName));
// Act
await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState);
- // Assert - Should not call CreateDefaultCollectionsBulkAsync when all users already have collections
+ // Assert - Should call repository with all user IDs (repository filters internally)
await collectionRepository
.Received(1)
- .GetDefaultCollectionSemaphoresAsync(Arg.Is>(ids => ids.Count() == 3));
-
- await collectionRepository
- .DidNotReceive()
- .CreateDefaultCollectionsBulkAsync(Arg.Any(), Arg.Any>(), Arg.Any());
+ .CreateDefaultCollectionsBulkAsync(
+ policyUpdate.OrganizationId,
+ Arg.Is>(ids => ids.Count() == 3),
+ _defaultUserCollectionName);
}
private static IEnumerable