From 66456b9d5d2c2236f94bd1ee6be68f4db1e3f6ab Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 22 Jul 2026 09:28:24 -0700 Subject: [PATCH 1/5] add /shared-folders endpoints and mark /collections endpoints obsolete --- src/Api/Vault/Controllers/CiphersController.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 7436ee993aa6..9c195afab453 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -760,7 +760,14 @@ public async Task PostShare(Guid id, [FromBody] CipherShare } [HttpPut("{id}/collections")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFolders(id, model); + } + + [HttpPut("{id}/shared-folders")] + public async Task PutSharedFolders(Guid id, [FromBody] CipherCollectionsRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); var cipher = await GetByIdAsync(id, user.Id); @@ -780,10 +787,17 @@ await _cipherService.SaveCollectionsAsync(cipher, } [HttpPost("{id}/collections")] - [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PostCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) { - return await PutCollections(id, model); + return await PutSharedFolders(id, model); + } + + [HttpPost("{id}/shared-folders")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostSharedFolders(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFolders(id, model); } [HttpPut("{id}/collections_v2")] From ef69f431fa50582cb6c24fb8768bc512f619a391 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 22 Jul 2026 09:31:11 -0700 Subject: [PATCH 2/5] add /shared-folders_v2 endpoints and marks /collections_v2 endpoints obsolete --- src/Api/Vault/Controllers/CiphersController.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 9c195afab453..af2a6c5b9b32 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -801,7 +801,14 @@ public async Task PostSharedFolders(Guid id, [FromBo } [HttpPut("{id}/collections_v2")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFolders_vNext(id, model); + } + + [HttpPut("{id}/shared-folders_v2")] + public async Task PutSharedFolders_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); var cipher = await GetByIdAsync(id, user.Id); @@ -829,10 +836,17 @@ await _cipherService.SaveCollectionsAsync(cipher, } [HttpPost("{id}/collections_v2")] - [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PostCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) { - return await PutCollections_vNext(id, model); + return await PutSharedFolders_vNext(id, model); + } + + [HttpPost("{id}/shared-folders_v2")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostSharedFolders_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFolders_vNext(id, model); } [HttpPut("{id}/collections-admin")] From ccb2f9d9a50e8495d44e173903919ddace957896 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 22 Jul 2026 09:32:49 -0700 Subject: [PATCH 3/5] add /shared-folders-admin endpoints and mark /collections-admin endpoints obsolete --- src/Api/Vault/Controllers/CiphersController.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index af2a6c5b9b32..6368098a7da4 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -850,7 +850,14 @@ public async Task PostSharedFolders_vNext(Gu } [HttpPut("{id}/collections-admin")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFoldersAdmin(id, model); + } + + [HttpPut("{id}/shared-folders-admin")] + public async Task PutSharedFoldersAdmin(string id, [FromBody] CipherCollectionsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id)); @@ -879,10 +886,17 @@ public async Task PutCollectionsAdmin(string id, } [HttpPost("{id}/collections-admin")] - [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PostCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) { - return await PutCollectionsAdmin(id, model); + return await PutSharedFoldersAdmin(id, model); + } + + [HttpPost("{id}/shared-folders-admin")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostSharedFoldersAdmin(string id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutSharedFoldersAdmin(id, model); } [HttpPost("bulk-collections")] From 119014e46ee70ff19f11b6006175093e0ca90d86 Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 22 Jul 2026 09:33:54 -0700 Subject: [PATCH 4/5] add /bulk-shared-folders endpoint and mark /bulk-collections endpoint obsolete --- src/Api/Vault/Controllers/CiphersController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 6368098a7da4..d857b16d482a 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -900,7 +900,14 @@ public async Task PostSharedFoldersAdmin(string } [HttpPost("bulk-collections")] + [Obsolete("Replaced by /shared-folders route; retained for backwards compatibility")] public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model) + { + await PostBulkSharedFolders(model); + } + + [HttpPost("bulk-shared-folders")] + public async Task PostBulkSharedFolders([FromBody] CipherBulkUpdateCollectionsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; await _cipherService.ValidateBulkCollectionAssignmentAsync(model.CollectionIds, model.CipherIds, userId); From 36e0b74007a3cedb81858f6b9ab79fed3f31b81e Mon Sep 17 00:00:00 2001 From: Jackson Engstrom Date: Wed, 22 Jul 2026 09:43:44 -0700 Subject: [PATCH 5/5] add tests --- .../Controllers/CiphersControllerTests.cs | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 926180400dd7..f5efe28f0baf 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -186,6 +186,363 @@ public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableTrue Assert.True(result.Unavailable); } + [Theory, BitAutoData] + public async Task PutSharedFolders_ShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, User user, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().OrganizationUser(Arg.Any()).Returns(false); + sutProvider.GetDependency().GetByIdAsync(id, user.Id).ReturnsNull(); + + var requestAction = async () => await sutProvider.Sut.PutSharedFolders(id, model); + + await Assert.ThrowsAsync(requestAction); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + } + + [Theory, BitAutoData] + public async Task PutSharedFolders_ShouldSaveUpdatedCipherAndReturnDetails(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + + var result = await sutProvider.Sut.PutSharedFolders(id, model); + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutSharedFolders_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, User user, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().OrganizationUser(Arg.Any()).Returns(false); + sutProvider.GetDependency().GetByIdAsync(id, user.Id).ReturnsNull(); + + var requestAction = async () => await sutProvider.Sut.PutSharedFolders_vNext(id, model); + + await Assert.ThrowsAsync(requestAction); + } + + [Theory, BitAutoData] + public async Task PutSharedFolders_vNextShouldSaveUpdatedCipher(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + + await sutProvider.Sut.PutSharedFolders_vNext(id, model); + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + } + + [Theory, BitAutoData] + public async Task PutSharedFolders_vNextReturnOptionalDetailsCipherUnavailableFalse(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + + var result = await sutProvider.Sut.PutSharedFolders_vNext(id, model); + + Assert.IsType(result); + Assert.False(result.Unavailable); + } + + [Theory, BitAutoData] + public async Task PutSharedFolders_vNextReturnOptionalDetailsCipherUnavailableTrue(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails, [(CipherDetails)null]); + + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + + var result = await sutProvider.Sut.PutSharedFolders_vNext(id, model); + + Assert.IsType(result); + Assert.True(result.Unavailable); + } + + [Theory, BitAutoData] + public async Task PutSharedFoldersAdmin_ShouldThrowNotFoundWhenCipherIsNull(Guid id, CipherCollectionsRequestModel model, Guid userId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(Arg.Any()).ReturnsNull(); + + var requestAction = async () => await sutProvider.Sut.PutSharedFoldersAdmin(id.ToString(), model); + + await Assert.ThrowsAsync(requestAction); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + } + + [Theory, BitAutoData] + public async Task PutSharedFoldersAdmin_ShouldSaveUpdatedCipherAndReturnDetails(Guid userId, SutProvider sutProvider) + { + var (id, model) = SetupSharedFoldersAdminSuccess(userId, sutProvider); + + var result = await sutProvider.Sut.PutSharedFoldersAdmin(id.ToString(), model); + + await sutProvider.GetDependency().Received(1).SaveCollectionsAsync( + Arg.Any(), Arg.Any>(), userId, true); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostBulkSharedFolders_WhenAdding_AddsCollectionsForManyCiphers(Guid userId, SutProvider sutProvider) + { + var model = SetupBulkSharedFoldersSuccess(userId, removeCollections: false, sutProvider); + + await sutProvider.Sut.PostBulkSharedFolders(model); + + await sutProvider.GetDependency().Received(1) + .AddCollectionsForManyCiphersAsync(model.OrganizationId, model.CipherIds, model.CollectionIds); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .RemoveCollectionsForManyCiphersAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task PostBulkSharedFolders_WhenRemoving_RemovesCollectionsForManyCiphers(Guid userId, SutProvider sutProvider) + { + var model = SetupBulkSharedFoldersSuccess(userId, removeCollections: true, sutProvider); + + await sutProvider.Sut.PostBulkSharedFolders(model); + + await sutProvider.GetDependency().Received(1) + .RemoveCollectionsForManyCiphersAsync(model.OrganizationId, model.CipherIds, model.CollectionIds); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .AddCollectionsForManyCiphersAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task PostBulkSharedFolders_WhenUnauthorized_ThrowsNotFound(Guid userId, CipherBulkUpdateCollectionsRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + // No organization relationship configured, so the user cannot modify the ciphers + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + var requestAction = async () => await sutProvider.Sut.PostBulkSharedFolders(model); + + await Assert.ThrowsAsync(requestAction); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .AddCollectionsForManyCiphersAsync(default, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .RemoveCollectionsForManyCiphersAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task PutCollections_DelegatesToSharedFolders(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PutCollections(id, model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostCollections_DelegatesToSharedFolders(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostCollections(id, model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostSharedFolders_DelegatesToPutSharedFolders(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostSharedFolders(id, model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostCollections_vNextDelegatesToSharedFolders(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostCollections_vNext(id, model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostSharedFolders_vNextDelegatesToPutSharedFolders(Guid id, CipherCollectionsRequestModel model, Guid userId, SutProvider sutProvider) + { + SetupUserAndOrgMocks(id, userId, sutProvider); + var cipherDetails = CreateCipherDetailsMock(id, userId); + sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); + sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostSharedFolders_vNext(id, model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().ReceivedWithAnyArgs().SaveCollectionsAsync(default, default, default, default); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutCollectionsAdmin_DelegatesToSharedFolders(Guid userId, SutProvider sutProvider) + { + var (id, model) = SetupSharedFoldersAdminSuccess(userId, sutProvider); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PutCollectionsAdmin(id.ToString(), model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().Received(1).SaveCollectionsAsync( + Arg.Any(), Arg.Any>(), userId, true); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostCollectionsAdmin_DelegatesToSharedFolders(Guid userId, SutProvider sutProvider) + { + var (id, model) = SetupSharedFoldersAdminSuccess(userId, sutProvider); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostCollectionsAdmin(id.ToString(), model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().Received(1).SaveCollectionsAsync( + Arg.Any(), Arg.Any>(), userId, true); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostSharedFoldersAdmin_DelegatesToPutSharedFoldersAdmin(Guid userId, SutProvider sutProvider) + { + var (id, model) = SetupSharedFoldersAdminSuccess(userId, sutProvider); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + var result = await sutProvider.Sut.PostSharedFoldersAdmin(id.ToString(), model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().Received(1).SaveCollectionsAsync( + Arg.Any(), Arg.Any>(), userId, true); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostBulkCollections_DelegatesToPostBulkSharedFolders(Guid userId, SutProvider sutProvider) + { + var model = SetupBulkSharedFoldersSuccess(userId, removeCollections: false, sutProvider); + +#pragma warning disable CS0618 // Testing the obsolete backwards-compatible route + await sutProvider.Sut.PostBulkCollections(model); +#pragma warning restore CS0618 + + await sutProvider.GetDependency().Received(1) + .AddCollectionsForManyCiphersAsync(model.OrganizationId, model.CipherIds, model.CollectionIds); + } + + private const string _loginCipherDataJson = @"{""Username"":""testuser"",""Password"":""securepassword123""}"; + + private (Guid id, CipherCollectionsRequestModel model) SetupSharedFoldersAdminSuccess(Guid userId, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + var cipherId = Guid.NewGuid(); + var collectionId = Guid.NewGuid(); + + var cipher = new CipherOrganizationDetails + { + Id = cipherId, + OrganizationId = organizationId, + Type = CipherType.Login, + Data = _loginCipherDataJson + }; + + var model = new CipherCollectionsRequestModel { CollectionIds = new[] { collectionId.ToString() } }; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organizationId) + .Returns(new CurrentContextOrganization { Id = organizationId, Type = OrganizationUserType.Owner }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId, AllowAdminAccessToAllCollectionItems = true }); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherId).Returns(cipher); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId) + .Returns(new List { cipher }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId) + .Returns(new List { new Collection { Id = collectionId, OrganizationId = organizationId } }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId) + .Returns(new List()); + + return (cipherId, model); + } + + private CipherBulkUpdateCollectionsRequestModel SetupBulkSharedFoldersSuccess(Guid userId, bool removeCollections, SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + var cipherId = Guid.NewGuid(); + var collectionId = Guid.NewGuid(); + + var model = new CipherBulkUpdateCollectionsRequestModel + { + OrganizationId = organizationId, + CipherIds = new[] { cipherId }, + CollectionIds = new[] { collectionId }, + RemoveCollections = removeCollections + }; + + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().GetOrganization(organizationId) + .Returns(new CurrentContextOrganization { Id = organizationId, Type = OrganizationUserType.Owner }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId, AllowAdminAccessToAllCollectionItems = true }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId) + .Returns(new List { new Cipher { Id = cipherId, OrganizationId = organizationId } }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId) + .Returns(new List { new Collection { Id = collectionId, OrganizationId = organizationId } }); + + return model; + } + private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId);