diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 32591c6a2..4a416c6df 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -696,7 +696,7 @@ public class CiphersController : Controller await Request.GetFileAsync(async (stream, fileName, key) => { - await _cipherService.CreateAttachmentShareAsync(cipher, stream, + await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key, Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId); }); } diff --git a/src/Core/Vault/Entities/Cipher.cs b/src/Core/Vault/Entities/Cipher.cs index 4ef4a1edb..15cbba411 100644 --- a/src/Core/Vault/Entities/Cipher.cs +++ b/src/Core/Vault/Entities/Cipher.cs @@ -45,6 +45,10 @@ public class Cipher : ITableObject, ICloneable foreach (var kvp in _attachmentData) { kvp.Value.AttachmentId = kvp.Key; + if (kvp.Value.TempMetadata != null) + { + kvp.Value.TempMetadata.AttachmentId = kvp.Key; + } } return _attachmentData; } diff --git a/src/Core/Vault/Models/Data/CipherAttachment.cs b/src/Core/Vault/Models/Data/CipherAttachment.cs index 78817aeb7..6450efe63 100644 --- a/src/Core/Vault/Models/Data/CipherAttachment.cs +++ b/src/Core/Vault/Models/Data/CipherAttachment.cs @@ -31,5 +31,10 @@ public class CipherAttachment // This is stored alongside metadata as an identifier. It does not need repeating in serialization [JsonIgnore] public string AttachmentId { get; set; } + + /// + /// Temporary metadata used to store original metadata on migrations from a user-owned attachment to an organization-owned one + /// + public MetaData TempMetadata { get; set; } } } diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 165be9a59..3168e50ea 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -14,8 +14,8 @@ public interface ICipherService string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId); Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, Guid savingUserId, bool orgAdmin = false); - Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, string attachmentId, - Guid organizationShareId); + Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, + string attachmentId, Guid organizationShareId); Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index f0e84eb63..5686497c6 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -258,8 +258,8 @@ public class CipherService : ICipherService await _pushService.PushSyncCipherUpdateAsync(cipher, null); } - public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, long requestLength, - string attachmentId, Guid organizationId) + public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, + long requestLength, string attachmentId, Guid organizationId) { try { @@ -296,8 +296,28 @@ public class CipherService : ICipherService throw new BadRequestException($"Cipher does not own specified attachment"); } + var originalAttachmentMetadata = attachments[attachmentId]; + + if (originalAttachmentMetadata.TempMetadata != null) + { + throw new BadRequestException("Another process is trying to migrate this attachment"); + } + + // Clone metadata to be modified and saved into the TempMetadata, + // we cannot change the metadata here directly because if the subsequent endpoint fails + // to be called, then the metadata would stay corrupted. + var attachmentMetadata = CoreHelpers.CloneObject(originalAttachmentMetadata); + attachmentMetadata.AttachmentId = originalAttachmentMetadata.AttachmentId; + originalAttachmentMetadata.TempMetadata = attachmentMetadata; + + if (key != null) + { + attachmentMetadata.Key = key; + attachmentMetadata.FileName = fileName; + } + await _attachmentStorageService.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, - attachments[attachmentId]); + attachmentMetadata); // Previous call may alter metadata var updatedAttachment = new CipherAttachment @@ -306,7 +326,7 @@ public class CipherService : ICipherService UserId = cipher.UserId, OrganizationId = cipher.OrganizationId, AttachmentId = attachmentId, - AttachmentData = JsonSerializer.Serialize(attachments[attachmentId]) + AttachmentData = JsonSerializer.Serialize(originalAttachmentMetadata) }; await _cipherRepository.UpdateAttachmentAsync(updatedAttachment); @@ -489,10 +509,10 @@ public class CipherService : ICipherService IEnumerable collectionIds, Guid sharingUserId, DateTime? lastKnownRevisionDate) { var attachments = cipher.GetAttachments(); - var hasOldAttachments = attachments?.Any(a => a.Key == null) ?? false; + var hasOldAttachments = attachments?.Values?.Any(a => a.Key == null) ?? false; var updatedCipher = false; var migratedAttachments = false; - var originalAttachments = CoreHelpers.CloneObject(attachments); + var originalAttachments = CoreHelpers.CloneObject(originalCipher.GetAttachments()); try { @@ -502,6 +522,21 @@ public class CipherService : ICipherService cipher.UserId = sharingUserId; cipher.OrganizationId = organizationId; cipher.RevisionDate = DateTime.UtcNow; + + if (hasOldAttachments) + { + var attachmentsWithUpdatedMetadata = originalCipher.GetAttachments(); + var attachmentsToUpdateMetadata = CoreHelpers.CloneObject(attachments); + foreach (var updatedMetadata in attachmentsWithUpdatedMetadata.Where(a => a.Value?.TempMetadata != null)) + { + if (attachmentsToUpdateMetadata.ContainsKey(updatedMetadata.Key)) + { + attachmentsToUpdateMetadata[updatedMetadata.Key] = updatedMetadata.Value.TempMetadata; + } + } + cipher.SetAttachments(attachmentsToUpdateMetadata); + } + if (!await _cipherRepository.ReplaceAsync(cipher, collectionIds)) { throw new BadRequestException("Unable to save."); @@ -513,25 +548,36 @@ public class CipherService : ICipherService if (hasOldAttachments) { // migrate old attachments - foreach (var attachment in attachments.Where(a => a.Key == null)) + foreach (var attachment in attachments.Values.Where(a => a.TempMetadata != null).Select(a => a.TempMetadata)) { await _attachmentStorageService.StartShareAttachmentAsync(cipher.Id, organizationId, - attachment.Value); + attachment); migratedAttachments = true; } // commit attachment migration await _attachmentStorageService.CleanupAsync(cipher.Id); } - - // push - await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); } catch { // roll everything back if (updatedCipher) { + if (hasOldAttachments) + { + foreach (var item in originalAttachments) + { + item.Value.TempMetadata = null; + } + originalCipher.SetAttachments(originalAttachments); + } + + var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(sharingUserId, originalCipher.Id); + var currentCollectionIdsForCipher = currentCollectionsForCipher.Select(c => c.CollectionId).ToList(); + currentCollectionIdsForCipher.RemoveAll(id => collectionIds.Contains(id)); + + await _collectionCipherRepository.UpdateCollectionsAsync(originalCipher.Id, sharingUserId, currentCollectionIdsForCipher); await _cipherRepository.ReplaceAsync(originalCipher); } @@ -546,7 +592,7 @@ public class CipherService : ICipherService await _organizationRepository.UpdateStorageAsync(organizationId); } - foreach (var attachment in attachments.Where(a => a.Key == null)) + foreach (var attachment in attachments.Where(a => a.Value.Key == null)) { await _attachmentStorageService.RollbackShareAttachmentAsync(cipher.Id, organizationId, attachment.Value, originalAttachments[attachment.Key].ContainerName); @@ -555,6 +601,9 @@ public class CipherService : ICipherService await _attachmentStorageService.CleanupAsync(cipher.Id); throw; } + + // push + await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); } public async Task ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos, diff --git a/test/Core.Test/Vault/AutoFixture/CipherAttachmentMetaDataFixtures.cs b/test/Core.Test/Vault/AutoFixture/CipherAttachmentMetaDataFixtures.cs index 2aa1e0076..58ed9f0a1 100644 --- a/test/Core.Test/Vault/AutoFixture/CipherAttachmentMetaDataFixtures.cs +++ b/test/Core.Test/Vault/AutoFixture/CipherAttachmentMetaDataFixtures.cs @@ -14,6 +14,10 @@ public class MetaData : ICustomization public void Customize(IFixture fixture) { fixture.Customize(composer => ComposerAction(fixture, composer)); + fixture.Behaviors.OfType() + .ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior(1)); } } diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 8ab34a130..c866fa282 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -1,7 +1,9 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Test.AutoFixture.CipherFixtures; +using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; @@ -44,7 +46,12 @@ public class CipherServiceTests Organization organization, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); + cipher.SetAttachments(new Dictionary + { + [Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { } + }); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, @@ -105,11 +112,438 @@ public class CipherServiceTests cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true); sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + cipher.SetAttachments(new Dictionary + { + [Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { } + }); await sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, lastKnownRevisionDate); await cipherRepository.Received(1).ReplaceAsync(cipher, collectionIds); } + [Theory] + [BitAutoData("Correct Time")] + public async Task ShareAsync_FailReplace_Throws(string revisionDateString, + SutProvider sutProvider, Cipher cipher, Organization organization, List collectionIds) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var cipherRepository = sutProvider.GetDependency(); + cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(false); + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + cipher.SetAttachments(new Dictionary + { + [Guid.NewGuid().ToString()] = new CipherAttachment.MetaData { } + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, + lastKnownRevisionDate)); + Assert.Contains("Unable to save", exception.Message); + } + + [Theory] + [BitAutoData("Correct Time")] + public async Task ShareAsync_HasV0Attachments_ReplaceAttachmentMetadataWithNewOneBeforeSavingCipher(string revisionDateString, + SutProvider sutProvider, Cipher cipher, Organization organization, List collectionIds) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var originalCipher = CoreHelpers.CloneObject(cipher); + var cipherRepository = sutProvider.GetDependency(); + cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var pushNotificationService = sutProvider.GetDependency(); + + var v0AttachmentId = Guid.NewGuid().ToString(); + var anotherAttachmentId = Guid.NewGuid().ToString(); + cipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted" + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + originalCipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + await sutProvider.Sut.ShareAsync(originalCipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, + lastKnownRevisionDate); + + await cipherRepository.Received().ReplaceAsync(Arg.Is(c => + c.GetAttachments()[v0AttachmentId].Key == "NewAttachmentKey" + && + c.GetAttachments()[v0AttachmentId].FileName == "AFileNameRe-EncryptedWithOrgKey") + , collectionIds); + + await pushNotificationService.Received(1).PushSyncCipherUpdateAsync(cipher, collectionIds); + } + + [Theory] + [BitAutoData("Correct Time")] + public async Task ShareAsync_HasV0Attachments_StartSharingThoseAttachments(string revisionDateString, + SutProvider sutProvider, Cipher cipher, Organization organization, List collectionIds) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var originalCipher = CoreHelpers.CloneObject(cipher); + var cipherRepository = sutProvider.GetDependency(); + cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var attachmentStorageService = sutProvider.GetDependency(); + + var v0AttachmentId = Guid.NewGuid().ToString(); + var anotherAttachmentId = Guid.NewGuid().ToString(); + cipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + originalCipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + await sutProvider.Sut.ShareAsync(originalCipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, + lastKnownRevisionDate); + + await attachmentStorageService.Received().StartShareAttachmentAsync(cipher.Id, + organization.Id, + Arg.Is(m => m.Key == "NewAttachmentKey" && m.FileName == "AFileNameRe-EncryptedWithOrgKey")); + + await attachmentStorageService.Received(0).StartShareAttachmentAsync(cipher.Id, + organization.Id, + Arg.Is(m => m.Key == "AwesomeKey" && m.FileName == "AnotherFilename")); + + await attachmentStorageService.Received().CleanupAsync(cipher.Id); + } + + [Theory] + [BitAutoData("Correct Time")] + public async Task ShareAsync_HasV0Attachments_StartShareThrows_PerformsRollback_Rethrows(string revisionDateString, + SutProvider sutProvider, Cipher cipher, Organization organization, List collectionIds) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var originalCipher = CoreHelpers.CloneObject(cipher); + var cipherRepository = sutProvider.GetDependency(); + cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true); + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + var attachmentStorageService = sutProvider.GetDependency(); + var collectionCipherRepository = sutProvider.GetDependency(); + collectionCipherRepository.GetManyByUserIdCipherIdAsync(cipher.UserId.Value, cipher.Id).Returns( + Task.FromResult((ICollection)new List + { + new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = collectionIds[0] + }, + new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = Guid.NewGuid() + } + })); + + var v0AttachmentId = Guid.NewGuid().ToString(); + var anotherAttachmentId = Guid.NewGuid().ToString(); + cipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + originalCipher.SetAttachments(new Dictionary + { + [v0AttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + attachmentStorageService.StartShareAttachmentAsync(cipher.Id, + organization.Id, + Arg.Is(m => m.AttachmentId == v0AttachmentId)) + .Returns(Task.FromException(new InvalidOperationException("ex from StartShareAttachmentAsync"))); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, + lastKnownRevisionDate)); + Assert.Contains("ex from StartShareAttachmentAsync", exception.Message); + + await collectionCipherRepository.Received().UpdateCollectionsAsync(cipher.Id, cipher.UserId.Value, + Arg.Is>(ids => ids.Count == 1 && ids[0] != collectionIds[0])); + + await cipherRepository.Received().ReplaceAsync(Arg.Is(c => + c.GetAttachments()[v0AttachmentId].Key == null + && + c.GetAttachments()[v0AttachmentId].FileName == "AFileNameEncrypted" + && + c.GetAttachments()[v0AttachmentId].TempMetadata == null) + ); + } + + [Theory] + [BitAutoData("Correct Time")] + public async Task ShareAsync_HasSeveralV0Attachments_StartShareThrowsOnSecondOne_PerformsRollback_Rethrows(string revisionDateString, + SutProvider sutProvider, Cipher cipher, Organization organization, List collectionIds) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var originalCipher = CoreHelpers.CloneObject(cipher); + var cipherRepository = sutProvider.GetDependency(); + cipherRepository.ReplaceAsync(cipher, collectionIds).Returns(true); + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + var attachmentStorageService = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var collectionCipherRepository = sutProvider.GetDependency(); + collectionCipherRepository.GetManyByUserIdCipherIdAsync(cipher.UserId.Value, cipher.Id).Returns( + Task.FromResult((ICollection)new List + { + new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = collectionIds[0] + }, + new CollectionCipher + { + CipherId = cipher.Id, + CollectionId = Guid.NewGuid() + } + })); + + var v0AttachmentId1 = Guid.NewGuid().ToString(); + var v0AttachmentId2 = Guid.NewGuid().ToString(); + var anotherAttachmentId = Guid.NewGuid().ToString(); + cipher.SetAttachments(new Dictionary + { + [v0AttachmentId1] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId1, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId1, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [v0AttachmentId2] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId2, + ContainerName = "attachments", + FileName = "AFileNameEncrypted2", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId2, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey2", + Key = "NewAttachmentKey2" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + originalCipher.SetAttachments(new Dictionary + { + [v0AttachmentId1] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId1, + ContainerName = "attachments", + FileName = "AFileNameEncrypted", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId1, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey", + Key = "NewAttachmentKey" + } + }, + [v0AttachmentId2] = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId2, + ContainerName = "attachments", + FileName = "AFileNameEncrypted2", + TempMetadata = new CipherAttachment.MetaData + { + AttachmentId = v0AttachmentId2, + ContainerName = "attachments", + FileName = "AFileNameRe-EncryptedWithOrgKey2", + Key = "NewAttachmentKey2" + } + }, + [anotherAttachmentId] = new CipherAttachment.MetaData + { + AttachmentId = anotherAttachmentId, + Key = "AwesomeKey", + FileName = "AnotherFilename", + ContainerName = "attachments", + Size = 300, + Validated = true + } + }); + + attachmentStorageService.StartShareAttachmentAsync(cipher.Id, + organization.Id, + Arg.Is(m => m.AttachmentId == v0AttachmentId2)) + .Returns(Task.FromException(new InvalidOperationException("ex from StartShareAttachmentAsync"))); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ShareAsync(cipher, cipher, organization.Id, collectionIds, cipher.UserId.Value, + lastKnownRevisionDate)); + Assert.Contains("ex from StartShareAttachmentAsync", exception.Message); + + await collectionCipherRepository.Received().UpdateCollectionsAsync(cipher.Id, cipher.UserId.Value, + Arg.Is>(ids => ids.Count == 1 && ids[0] != collectionIds[0])); + + await cipherRepository.Received().ReplaceAsync(Arg.Is(c => + c.GetAttachments()[v0AttachmentId1].Key == null + && + c.GetAttachments()[v0AttachmentId1].FileName == "AFileNameEncrypted" + && + c.GetAttachments()[v0AttachmentId1].TempMetadata == null) + ); + + await cipherRepository.Received().ReplaceAsync(Arg.Is(c => + c.GetAttachments()[v0AttachmentId2].Key == null + && + c.GetAttachments()[v0AttachmentId2].FileName == "AFileNameEncrypted2" + && + c.GetAttachments()[v0AttachmentId2].TempMetadata == null) + ); + + await userRepository.UpdateStorageAsync(cipher.UserId.Value); + await organizationRepository.UpdateStorageAsync(organization.Id); + + await attachmentStorageService.Received().RollbackShareAttachmentAsync(cipher.Id, organization.Id, + Arg.Is(m => m.AttachmentId == v0AttachmentId1), Arg.Any()); + + await attachmentStorageService.Received().CleanupAsync(cipher.Id); + } + [Theory] [BitAutoData("")] [BitAutoData("Correct Time")]