1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-22 20:11:04 -05:00
bitwarden/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs
Graham Walker 818934487f
PM-18939 refactoring send service to 'cqrs' (#5652)
* PM-18939 refactoring send service to 'cqrs'

* PM-18939 fixing import issue with sendValidationService

* PM-18939 fixing code based on PR comments

* PM-18339 reverting to previous code in test

* PM-18939 adding XMLdocs to services

* PM-18939 reverting send validation methods

* PM-18939 updating code to match main

* PM-18939 reverting validateUserCanSaveAsync to match main

* PM-18939 fill our param and return sections of XMLdocs

* PM-18939 updating XMLdocs based on PR comments

* Update src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs

Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* Update src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs

Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* Update src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs

Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* Update src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs

Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* PM-18939 adding commits to change tuple to enum type

* PM-18939 resetting stream position to 0 when uploading file

* PM-18939 updating XMLdocs based on PR comments

* PM-18939 updating XMLdocs

* PM-18939 removing circular dependency

* PM-18939 fixing based on comments

* PM-18939 updating method name and documentation

---------

Co-authored-by:  Audrey  <ajensen@bitwarden.com>
2025-05-19 22:59:30 -05:00

1112 lines
44 KiB
C#

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<ISendRepository>();
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
_featureService = Substitute.For<IFeatureService>();
_sendValidationService = Substitute.For<ISendValidationService>();
_referenceEventService = Substitute.For<IReferenceEventService>();
_currentContext = Substitute.For<ICurrentContext>();
_sendCoreHelperService = Substitute.For<ISendCoreHelperService>();
_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<BadRequestException>(() =>
_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<Send>());
}
[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<ReferenceEvent>(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<ReferenceEvent>());
}
}
[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<BadRequestException>(() =>
_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<Send>());
}
else
{
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
}
// Verify push notification wasn't sent
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<ReferenceEvent>(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<ReferenceEvent>());
}
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
}
[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<ReferenceEvent>(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<ReferenceEvent>());
}
}
// 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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
// Verify push notification wasn't sent
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
// Verify reference event service wasn't called
await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any<ReferenceEvent>());
}
[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<ReferenceEvent>(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<ReferenceEvent>());
}
}
[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<ReferenceEvent>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().CreateAsync(Arg.Any<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<BadRequestException>(() =>
_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<Send>());
await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
}
[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<Send>(), Arg.Any<string>())
.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<Send>(), Arg.Any<string>())
.Throws(new Exception("Storage service unavailable"));
// Act & Assert
await Assert.ThrowsAsync<Exception>(() =>
_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<BadRequestException>(() =>
_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<Stream>(), Arg.Any<Send>(), Arg.Any<string>());
}
[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<BadRequestException>(() =>
_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<Stream>(), Arg.Any<Send>(), Arg.Any<string>());
}
[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<BadRequestException>(() =>
_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<Stream>(), Arg.Any<Send>(), Arg.Any<string>());
}
[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<Stream>(s => s == stream && s.Position == 0), // Ensure stream position is reset
Arg.Is<Send>(s => s.Id == sendId && s.UserId == userId),
Arg.Is<string>(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<Stream>(s => s == stream && s.Position == 0), // Ensure stream position is reset
Arg.Is<Send>(s => s.Id == sendId && s.UserId == userId),
Arg.Is<string>(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<Stream>(), Arg.Any<Send>(), Arg.Any<string>())
.Returns(Task.CompletedTask);
// Configure validation to fail due to file size mismatch
_nonAnonymousSendCommand.ConfirmFileSize(send)
.Returns(false);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
_nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send));
Assert.Equal("File received does not match expected file length.", exception.Message);
}
}