From bc9493480818885cf24486753c64f9dca1605377 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:57:43 -0700 Subject: [PATCH] [PM-31787] Users can access the sends after the limit was reached (#6958) * fix file type send increment behavior * fix text send access increment behavior * fix & update tests * cleanup unused service * fix broken test constructor expecting unused service --- src/Api/Tools/Controllers/SendsController.cs | 30 +- .../Interfaces/INonAnonymousSendCommand.cs | 40 ++- .../Commands/NonAnonymousSendCommand.cs | 18 +- .../Tools/Controllers/SendsControllerTests.cs | 154 +++++++- .../Services/NonAnonymousSendCommandTests.cs | 329 +++++++++++++++++- 5 files changed, 547 insertions(+), 24 deletions(-) diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index c04eae0904..af7fe8f12b 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -240,6 +240,11 @@ public class SendsController : Controller throw new BadRequestException("Could not locate send"); } + if (!INonAnonymousSendCommand.SendCanBeAccessed(send)) + { + throw new NotFoundException(); + } + var sendResponse = new SendAccessResponseModel(send); if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault()) { @@ -247,9 +252,19 @@ public class SendsController : Controller sendResponse.CreatorIdentifier = creator.Email; } - send.AccessCount++; - await _sendRepository.ReplaceAsync(send); - await _pushNotificationService.PushSyncSendUpdateAsync(send); + /* + * AccessCount is incremented differently for File and Text Send types: + * - Text Sends are incremented at every access + * - File Sends are incremented only when the file is downloaded + * + * Note that this endpoint is initially called for all Send types + */ + if (send.Type == SendType.Text) + { + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + } return new ObjectResult(sendResponse); } @@ -267,11 +282,12 @@ public class SendsController : Controller throw new BadRequestException("Could not locate send"); } - var url = await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId); + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); - send.AccessCount++; - await _sendRepository.ReplaceAsync(send); - await _pushNotificationService.PushSyncSendUpdateAsync(send); + if (result.Equals(SendAccessResult.Denied)) + { + throw new NotFoundException(); + } return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url }); } diff --git a/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs index 58693e619c..5ecf056268 100644 --- a/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs @@ -47,7 +47,45 @@ public interface INonAnonymousSendCommand /// when the file is confirmed, otherwise /// /// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of - /// an abundance of caution. + /// an abundance of caution. /// Task ConfirmFileSize(Send send); + + /// + /// If a File type Send can be downloaded, retrieves the download URL. + /// + /// The this command acts upon + /// The fileId to be downloaded + /// + /// A tuple wrapping the download URL string and indicating whether access was granted + /// + /// + /// This method is intended for authenticated endpoints where authentication has already been validated. + /// Returns when the Send is disabled, MaxAccessCount has been reached, + /// expiration date has passed, or deletion date has been reached. + /// + Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId); + + /// + /// Determines whether a can be accessed based on its current state. + /// + /// The to evaluate for access + /// if the Send can be accessed, otherwise + /// + /// This method checks if the Send is disabled, if MaxAccessCount has been reached, + /// if the expiration date has passed, or if the deletion date has been reached. + /// + static bool SendCanBeAccessed(Send send) + { + var now = DateTime.UtcNow; + if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || + send.Disabled || + send.DeletionDate < now) + { + return false; + } + + return true; + } } diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs index 9655d155e6..21ca1ca3fb 100644 --- a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -27,7 +27,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand public NonAnonymousSendCommand(ISendRepository sendRepository, ISendFileStorageService sendFileStorageService, IPushNotificationService pushNotificationService, - ISendAuthorizationService sendAuthorizationService, ISendValidationService sendValidationService, ISendCoreHelperService sendCoreHelperService, ILogger logger) @@ -181,4 +180,21 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand return valid; } + public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId) + { + if (send.Type != SendType.File) + { + throw new BadRequestException("Can only get a download URL for a file type of Send"); + } + + if (!INonAnonymousSendCommand.SendCanBeAccessed(send)) + { + return (null, SendAccessResult.Denied); + } + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), SendAccessResult.Granted); + } } diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 9322948037..3d77ac2343 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -903,6 +903,106 @@ public class SendsControllerTests : IDisposable Assert.Equal(creator.Email, response.CreatorIdentifier); } + [Theory, AutoData] + public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = true, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithMaxAccessCountReached_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 10, + MaxAccessCount = 10 + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task AccessUsingAuth_WithDeletionDatePassed_ThrowsNotFoundException(Guid sendId) + { + var send = new Send + { + Id = sendId, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), + DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithValidFileId_ReturnsDownloadUrl( Guid sendId, string fileId, string expectedUrl) @@ -922,7 +1022,8 @@ public class SendsControllerTests : IDisposable var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); - _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); + _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId) + .Returns((expectedUrl, SendAccessResult.Granted)); var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId); @@ -932,7 +1033,7 @@ public class SendsControllerTests : IDisposable Assert.Equal(fileId, response.Id); Assert.Equal(expectedUrl, response.Url); await _sendRepository.Received(1).GetByIdAsync(sendId); - await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId); + await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId); } [Theory, AutoData] @@ -948,13 +1049,13 @@ public class SendsControllerTests : IDisposable Assert.Equal("Could not locate send", exception.Message); await _sendRepository.Received(1).GetByIdAsync(sendId); - await _sendFileStorageService.DidNotReceive() + await _nonAnonymousSendCommand.DidNotReceive() .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); } [Theory, AutoData] - public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_StillReturnsResponse( - Guid sendId, string fileId, string expectedUrl) + public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_ThrowsBadRequestException( + Guid sendId, string fileId) { var send = new Send { @@ -970,15 +1071,44 @@ public class SendsControllerTests : IDisposable var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); - _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); + _nonAnonymousSendCommand + .When(x => x.GetSendFileDownloadUrlAsync(send, fileId)) + .Do(x => throw new BadRequestException("Can only get a download URL for a file type of Send")); - var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId); + var exception = await Assert.ThrowsAsync( + () => _sut.GetSendFileDownloadDataUsingAuth(fileId)); - Assert.NotNull(result); - var objectResult = Assert.IsType(result); - var response = Assert.IsType(objectResult.Value); - Assert.Equal(fileId, response.Id); - Assert.Equal(expectedUrl, response.Url); + Assert.Equal("Can only get a download URL for a file type of Send", exception.Message); + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId); + } + + [Theory, AutoData] + public async Task GetSendFileDownloadDataUsingAuth_WithAccessDenied_ThrowsNotFoundException( + Guid sendId, string fileId) + { + var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = fileId, Size = 2048 }; + var send = new Send + { + Id = sendId, + Type = SendType.File, + Data = JsonSerializer.Serialize(fileData), + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + Disabled = false, + AccessCount = 0, + MaxAccessCount = null + }; + var user = CreateUserWithSendIdClaim(sendId); + _sut.ControllerContext = CreateControllerContextWithUser(user); + _sendRepository.GetByIdAsync(sendId).Returns(send); + _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId) + .Returns((null, SendAccessResult.Denied)); + + await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); + + await _sendRepository.Received(1).GetByIdAsync(sendId); + await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId); } diff --git a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs index 1ad6a08516..9bebe5560c 100644 --- a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs +++ b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs @@ -11,6 +11,7 @@ using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Logging; @@ -28,7 +29,6 @@ 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 ICurrentContext _currentContext; @@ -42,7 +42,6 @@ public class NonAnonymousSendCommandTests _sendRepository = Substitute.For(); _sendFileStorageService = Substitute.For(); _pushNotificationService = Substitute.For(); - _sendAuthorizationService = Substitute.For(); _featureService = Substitute.For(); _sendValidationService = Substitute.For(); _currentContext = Substitute.For(); @@ -53,7 +52,6 @@ public class NonAnonymousSendCommandTests _sendRepository, _sendFileStorageService, _pushNotificationService, - _sendAuthorizationService, _sendValidationService, _sendCoreHelperService, _logger @@ -1093,4 +1091,329 @@ public class NonAnonymousSendCommandTests Assert.Equal("File received does not match expected file length.", exception.Message); } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithTextSend_ThrowsBadRequest() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text, + UserId = Guid.NewGuid() + }; + var fileId = "somefile123"; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId)); + + Assert.Equal("Can only get a download URL for a file type of Send", exception.Message); + + // Verify no storage service methods were called + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithDisabledSend_ReturnsDenied() + { + // Arrange + var fileId = "file123"; + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Disabled = true, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); + + // Assert + Assert.Null(url); + Assert.Equal(SendAccessResult.Denied, result); + + // Verify no repository updates occurred + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithMaxAccessCountReached_ReturnsDenied() + { + // Arrange + var fileId = "file123"; + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 5, + MaxAccessCount = 5 + }; + + // Act + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); + + // Assert + Assert.Null(url); + Assert.Equal(SendAccessResult.Denied, result); + + // Verify no repository updates occurred + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithExpiredSend_ReturnsDenied() + { + // Arrange + var fileId = "file123"; + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); + + // Assert + Assert.Null(url); + Assert.Equal(SendAccessResult.Denied, result); + + // Verify no repository updates occurred + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithDeletionDatePassed_ReturnsDenied() + { + // Arrange + var fileId = "file123"; + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed + ExpirationDate = null, + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); + + // Assert + Assert.Null(url); + Assert.Equal(SendAccessResult.Denied, result); + + // Verify no repository updates occurred + await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive() + .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_WithValidSend_ReturnsUrlAndIncrementsAccessCount() + { + // Arrange + var fileId = "file123"; + var expectedUrl = "https://download.example.com/file123"; + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 3, + MaxAccessCount = 10 + }; + + _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); + + // Act + var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId); + + // Assert + Assert.Equal(expectedUrl, url); + Assert.Equal(SendAccessResult.Granted, result); + + // Verify access count was incremented + Assert.Equal(4, send.AccessCount); + + // Verify repository was updated + await _sendRepository.Received(1).ReplaceAsync(send); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + + // Verify file storage service was called + await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId); + } + + [Fact] + public void SendCanBeAccessed_WithDisabledSend_ReturnsFalse() + { + // Arrange + var send = new Send + { + Disabled = true, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.False(result); + } + + [Fact] + public void SendCanBeAccessed_WithMaxAccessCountReached_ReturnsFalse() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 10, + MaxAccessCount = 10 + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.False(result); + } + + [Fact] + public void SendCanBeAccessed_WithExpiredSend_ReturnsFalse() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(-1), + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.False(result); + } + + [Fact] + public void SendCanBeAccessed_WithDeletionDatePassed_ReturnsFalse() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(-1), + ExpirationDate = null, + AccessCount = 0, + MaxAccessCount = null + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.False(result); + } + + [Fact] + public void SendCanBeAccessed_WithValidSend_ReturnsTrue() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = DateTime.UtcNow.AddDays(7), + AccessCount = 5, + MaxAccessCount = 10 + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.True(result); + } + + [Fact] + public void SendCanBeAccessed_WithNullMaxAccessCount_ReturnsTrue() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 100, + MaxAccessCount = null + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.True(result); + } + + [Fact] + public void SendCanBeAccessed_WithNullExpirationDate_ReturnsTrue() + { + // Arrange + var send = new Send + { + Disabled = false, + DeletionDate = DateTime.UtcNow.AddDays(7), + ExpirationDate = null, + AccessCount = 0, + MaxAccessCount = 10 + }; + + // Act + var result = INonAnonymousSendCommand.SendCanBeAccessed(send); + + // Assert + Assert.True(result); + } }