using System.Text.Json; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.CurrentContextFixtures; using Bit.Core.Test.Tools.AutoFixture.SendFixtures; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.SendFeatures.Commands; using Bit.Core.Tools.Services; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; namespace Bit.Core.Test.Tools.Services; [SutProviderCustomize] [CurrentContextCustomize] [UserSendCustomize] public class NonAnonymousSendCommandTests { private readonly ISendRepository _sendRepository; private readonly ISendFileStorageService _sendFileStorageService; private readonly IPushNotificationService _pushNotificationService; private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendValidationService _sendValidationService; private readonly IFeatureService _featureService; private readonly IReferenceEventService _referenceEventService; private readonly ICurrentContext _currentContext; private readonly ISendCoreHelperService _sendCoreHelperService; private readonly NonAnonymousSendCommand _nonAnonymousSendCommand; public NonAnonymousSendCommandTests() { _sendRepository = Substitute.For(); _sendFileStorageService = Substitute.For(); _pushNotificationService = Substitute.For(); _sendAuthorizationService = Substitute.For(); _featureService = Substitute.For(); _sendValidationService = Substitute.For(); _referenceEventService = Substitute.For(); _currentContext = Substitute.For(); _sendCoreHelperService = Substitute.For(); _nonAnonymousSendCommand = new NonAnonymousSendCommand( _sendRepository, _sendFileStorageService, _pushNotificationService, _sendAuthorizationService, _sendValidationService, _referenceEventService, _currentContext, _sendCoreHelperService ); } // Disable Send policy check [Theory] [InlineData(SendType.File)] [InlineData(SendType.Text)] public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType) { // Arrange var send = new Send { Id = default, Type = sendType, UserId = Guid.NewGuid() }; var user = new User { Id = send.UserId.Value, Email = "test@example.com" }; // Configure validation service to throw when DisableSend policy applies _sendValidationService.ValidateUserCanSaveAsync(send.UserId.Value, send) .Throws(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveSendAsync(send)); Assert.Contains("Enterprise Policy", exception.Message); // Verify the validation service was called await _sendValidationService.Received(1).ValidateUserCanSaveAsync(send.UserId.Value, send); // Verify repository was not called since exception was thrown await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); } [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableSend_DoesntApply_success(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, Data = "Text with Notes" }; var initialDate = DateTime.UtcNow.AddMinutes(-5); send.RevisionDate = initialDate; // Configure validation service to NOT throw (policy doesn't apply) _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); // Set up context for reference event _currentContext.ClientId.Returns("test-client"); _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); // Act await _nonAnonymousSendCommand.SaveSendAsync(send); // Assert // Verify validation was checked await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); if (isNewSend) { // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Id == userId && e.Type == ReferenceEventType.SendCreated && e.Source == ReferenceEventSource.User && e.SendType == send.Type && e.SendHasNotes == true && e.ClientId == "test-client" && e.ClientVersion == Version.Parse("1.0.0"))); } else { // For existing Sends await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableHideEmail_Applies_throws(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, HideEmail = true }; // Configure validation service to throw when HideEmail policy applies _sendValidationService.ValidateUserCanSaveAsync(userId, send) .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveSendAsync(send)); Assert.Contains("hide your email address", exception.Message); // Verify validation was called await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); // Verify repository was not called (exception prevented save) if (isNewSend) { await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); } else { await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); } // Verify push notification wasn't sent await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, HideEmail = true // Setting HideEmail to true }; var initialDate = DateTime.UtcNow.AddMinutes(-5); send.RevisionDate = initialDate; // Configure validation service to NOT throw (policy doesn't apply) _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); // Set up context for reference event _currentContext.ClientId.Returns("test-client"); _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); // Act await _nonAnonymousSendCommand.SaveSendAsync(send); // Assert // Verify validation was checked await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); if (isNewSend) { // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Id == userId && e.Type == ReferenceEventType.SendCreated && e.Source == ReferenceEventSource.User && e.SendType == send.Type && e.HasPassword == false && e.ClientId == "test-client" && e.ClientVersion == Version.Parse("1.0.0"))); } else { // For existing Sends await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } [Theory] [InlineData(SendType.File)] [InlineData(SendType.Text)] public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = default, Type = sendType, UserId = userId }; // Configure validation service to throw when DisableSend policy applies in vNext implementation _sendValidationService.ValidateUserCanSaveAsync(userId, send) .Returns(Task.FromException(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."))); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveSendAsync(send)); Assert.Contains("Enterprise Policy", exception.Message); // Verify validation service was called await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); // Verify repository and notification methods were not called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, Data = "Text with Notes" }; var initialDate = DateTime.UtcNow.AddMinutes(-5); send.RevisionDate = initialDate; // Configure validation service to return success for vNext implementation _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); // Set up context for reference event _currentContext.ClientId.Returns("test-client"); _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); // Enable feature flag for policy requirements (vNext path) _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); // Act await _nonAnonymousSendCommand.SaveSendAsync(send); // Assert // Verify validation was checked with vNext path await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); if (isNewSend) { // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Id == userId && e.Type == ReferenceEventType.SendCreated && e.Source == ReferenceEventSource.User && e.SendType == send.Type && e.SendHasNotes == true && e.ClientId == "test-client" && e.ClientVersion == Version.Parse("1.0.0"))); } else { // For existing Sends await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } // Send Options Policy - Disable Hide Email check [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, HideEmail = true }; // Enable feature flag for policy requirements (vNext path) _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); // Configure validation service to throw when DisableHideEmail policy applies in vNext implementation _sendValidationService.ValidateUserCanSaveAsync(userId, send) .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveSendAsync(send)); Assert.Contains("hide your email address", exception.Message); // Verify validation was called await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); // Verify repository was not called (exception prevented save) await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); // Verify push notification wasn't sent await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); // Verify reference event service wasn't called await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Theory] [InlineData(true)] // New Send (Id is default) [InlineData(false)] // Existing Send (Id is not default) public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(bool isNewSend) { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = isNewSend ? default : Guid.NewGuid(), Type = SendType.Text, UserId = userId, HideEmail = false // Email is not hidden, so policy doesn't block }; var initialDate = DateTime.UtcNow.AddMinutes(-5); send.RevisionDate = initialDate; // Enable feature flag for policy requirements (vNext path) _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); // Configure validation service to allow saves when HideEmail is false _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); // Set up context for reference event _currentContext.ClientId.Returns("test-client"); _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); // Act await _nonAnonymousSendCommand.SaveSendAsync(send); // Assert // Verify validation was called with vNext path await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); if (isNewSend) { // For new Sends await _sendRepository.Received(1).CreateAsync(send); await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => e.Id == userId && e.Type == ReferenceEventType.SendCreated && e.Source == ReferenceEventSource.User && e.SendType == send.Type && e.ClientId == "test-client" && e.ClientVersion == Version.Parse("1.0.0"))); } else { // For existing Sends await _sendRepository.Received(1).UpsertAsync(send); Assert.NotEqual(initialDate, send.RevisionDate); await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } } [Fact] public async Task SaveSendAsync_ExistingSend_Updates() { // Arrange var userId = Guid.NewGuid(); var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, Type = SendType.Text, UserId = userId, Data = "Some text data" }; var initialDate = DateTime.UtcNow.AddMinutes(-5); send.RevisionDate = initialDate; // Act await _nonAnonymousSendCommand.SaveSendAsync(send); // Assert // Verify validation was called await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); // Verify repository was called with updated send await _sendRepository.Received(1).UpsertAsync(send); // Check that the revision date was updated Assert.NotEqual(initialDate, send.RevisionDate); // Verify push notification was sent for the update await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); // Verify no reference event was raised (only happens for new sends) await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_TextType_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.Text, // Text type instead of File UserId = userId }; var fileData = new SendFileData(); var fileLength = 1024L; // 1KB // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("not of type \"file\"", exception.Message); // Verify no further methods were called await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 0L; // Empty file // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("No file data", exception.Message); // Verify no methods were called after validation failed await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 1024L; // 1KB // Configure validation service to throw when checking storage _sendValidationService.StorageRemainingForSendAsync(send) .Throws(new BadRequestException("You must have premium status to use file Sends.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("premium status", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 1024L; // 1KB // Configure validation service to pass storage check _sendValidationService.StorageRemainingForSendAsync(send).Returns(10240L); // 10KB remaining // Configure validation service to throw when checking user can save _sendValidationService.When(x => x.ValidateUserCanSaveAsync(userId, send)) .Throw(new BadRequestException("You must confirm your email before creating a Send.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("confirm your email", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify SaveSendAsync attempted to be called, triggering email validation await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); // Verify no repository or notification methods were called after validation failed await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 1024L; // 1KB // Configure validation service to return 0 storage remaining _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("Not enough storage available", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 1024L; // 1KB // Configure validation service to return less storage remaining than needed _sendValidationService.StorageRemainingForSendAsync(send).Returns(512L); // Only 512 bytes available // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("Not enough storage available", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 15L * 1024L * 1024L * 1024L; // 15GB // Configure validation service to return large but insufficient storage (10GB for self-hosted non-premium) _sendValidationService.StorageRemainingForSendAsync(send) .Returns(10L * 1024L * 1024L * 1024L); // 10GB remaining (self-hosted default) // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("Not enough storage available", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB // Configure validation service to return 1GB storage (cloud non-premium default) _sendValidationService.StorageRemainingForSendAsync(send) .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining (cloud default) // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("Not enough storage available", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest() { // Arrange var organizationId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, OrganizationId = organizationId }; var fileData = new SendFileData { FileName = "test.txt" }; const long fileLength = 1000; // Set up validation service to return 0 storage remaining // This simulates the case when an organization's max storage is null _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Equal("Not enough storage available.", exception.Message); // Verify the method was called exactly once await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); } [Fact] public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest() { // Arrange var orgId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, OrganizationId = orgId, UserId = null }; var fileData = new SendFileData(); var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB // Configure validation service to throw BadRequest when checking storage for org without storage _sendValidationService.StorageRemainingForSendAsync(send) .Throws(new BadRequestException("This organization cannot use file sends.")); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("This organization cannot use file sends", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest() { // Arrange var orgId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, OrganizationId = orgId, UserId = null }; var fileData = new SendFileData(); var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB // Configure validation service to return 1GB storage (org's max storage limit) _sendValidationService.StorageRemainingForSendAsync(send) .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); Assert.Contains("Not enough storage available", exception.Message); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify no further methods were called await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); } [Fact] public async Task SaveFileSendAsync_HasEnoughStorage_Success() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 500L * 1024L; // 500KB var expectedFileId = "generatedfileid"; var expectedUploadUrl = "https://upload.example.com/url"; // Configure storage validation to return more storage than needed _sendValidationService.StorageRemainingForSendAsync(send) .Returns(1024L * 1024L); // 1MB remaining // Configure file storage service to return upload URL _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) .Returns(expectedUploadUrl); // Set up string generator to return predictable file ID _sendCoreHelperService.SecureRandomString(32, false, false) .Returns(expectedFileId); // Act var result = await _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength); // Assert Assert.Equal(expectedUploadUrl, result); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify upload URL was requested await _sendFileStorageService.Received(1).GetSendFileUploadUrlAsync(send, expectedFileId); } [Fact] public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp() { // Arrange var userId = Guid.NewGuid(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = userId }; var fileData = new SendFileData(); var fileLength = 500L * 1024L; // 500KB var expectedFileId = "generatedfileid"; // Configure storage validation to return more storage than needed _sendValidationService.StorageRemainingForSendAsync(send) .Returns(1024L * 1024L); // 1MB remaining // Set up string generator to return predictable file ID _sendCoreHelperService.SecureRandomString(32, false, false) .Returns(expectedFileId); // Configure file storage service to throw exception when getting upload URL _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) .Throws(new Exception("Storage service unavailable")); // Act & Assert await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); // Verify storage validation was called await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); // Verify file was cleaned up after failure await _sendFileStorageService.Received(1).DeleteFileAsync(send, expectedFileId); } [Fact] public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest() { // Arrange Stream stream = new MemoryStream(); Send send = null; // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); Assert.Equal("Send does not have file data", exception.Message); // Verify no interactions with storage service await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest() { // Arrange Stream stream = new MemoryStream(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.File, UserId = Guid.NewGuid(), Data = null // Send exists but has null Data property }; // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); Assert.Equal("Send does not have file data", exception.Message); // Verify no interactions with storage service await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest() { // Arrange Stream stream = new MemoryStream(); var send = new Send { Id = Guid.NewGuid(), Type = SendType.Text, // Not a file type UserId = Guid.NewGuid(), Data = "{\"someData\":\"value\"}" // Has data, but not file data }; // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); Assert.Equal("Not a File Type Send.", exception.Message); // Verify no interactions with storage service await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] public async Task UpdateFileToExistingSendAsync_StreamPositionRestToZero_Success() { // Arrange var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); stream.Position = 2; var sendId = Guid.NewGuid(); var userId = Guid.NewGuid(); var fileId = "existingfileid123"; var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; var send = new Send { Id = sendId, UserId = userId, Type = SendType.File, Data = JsonSerializer.Serialize(sendFileData) }; // Setup validation to succeed _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); // Act await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); // Assert // Verify file was uploaded with correct parameters await _sendFileStorageService.Received(1).UploadNewFileAsync( Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset Arg.Is(s => s.Id == sendId && s.UserId == userId), Arg.Is(id => id == fileId) ); } [Fact] public async Task UploadFileToExistingSendAsync_Success() { // Arrange var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); stream.Position = 2; // Simulate a non-zero position var sendId = Guid.NewGuid(); var userId = Guid.NewGuid(); var fileId = "existingfileid123"; var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; var send = new Send { Id = sendId, UserId = userId, Type = SendType.File, Data = JsonSerializer.Serialize(sendFileData) }; _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); // Act await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); // Assert // Verify file was uploaded with correct parameters await _sendFileStorageService.Received(1).UploadNewFileAsync( Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset Arg.Is(s => s.Id == sendId && s.UserId == userId), Arg.Is(id => id == fileId) ); } [Fact] public async Task UpdateFileToExistingSendAsync_InvalidSize_ThrowsBadRequest() { // Arrange var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); var sendId = Guid.NewGuid(); var userId = Guid.NewGuid(); var fileId = "existingfileid123"; var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; var send = new Send { Id = sendId, UserId = userId, Type = SendType.File, Data = JsonSerializer.Serialize(sendFileData) }; // Configure storage service to upload successfully _sendFileStorageService.UploadNewFileAsync( Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask); // Configure validation to fail due to file size mismatch _nonAnonymousSendCommand.ConfirmFileSize(send) .Returns(false); // Act & Assert var exception = await Assert.ThrowsAsync(() => _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); Assert.Equal("File received does not match expected file length.", exception.Message); } }