1
0
mirror of https://github.com/bitwarden/server synced 2026-02-12 14:33:49 +00:00

[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
This commit is contained in:
John Harrington
2026-02-10 07:57:43 -07:00
committed by GitHub
parent e5cf9dff2e
commit bc94934808
5 changed files with 547 additions and 24 deletions

View File

@@ -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 });
}

View File

@@ -47,7 +47,45 @@ public interface INonAnonymousSendCommand
/// <returns><see langword="true" /> when the file is confirmed, otherwise <see langword="false" /></returns>
/// <remarks>
/// 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.
/// </remarks>
Task<bool> ConfirmFileSize(Send send);
/// <summary>
/// If a File type Send can be downloaded, retrieves the download URL.
/// </summary>
/// <param name="send">The <see cref="Send" /> this command acts upon</param>
/// <param name="fileId">The fileId to be downloaded</param>
/// <returns>
/// A tuple wrapping the download URL string and <see cref="SendAccessResult" /> indicating whether access was granted
/// </returns>
/// <remarks>
/// This method is intended for authenticated endpoints where authentication has already been validated.
/// Returns <see cref="SendAccessResult.Denied" /> when the Send is disabled, MaxAccessCount has been reached,
/// expiration date has passed, or deletion date has been reached.
/// </remarks>
Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId);
/// <summary>
/// Determines whether a <see cref="Send" /> can be accessed based on its current state.
/// </summary>
/// <param name="send">The <see cref="Send" /> to evaluate for access</param>
/// <returns><see langword="true" /> if the Send can be accessed, otherwise <see langword="false" /></returns>
/// <remarks>
/// 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.
/// </remarks>
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;
}
}

View File

@@ -27,7 +27,6 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
public NonAnonymousSendCommand(ISendRepository sendRepository,
ISendFileStorageService sendFileStorageService,
IPushNotificationService pushNotificationService,
ISendAuthorizationService sendAuthorizationService,
ISendValidationService sendValidationService,
ISendCoreHelperService sendCoreHelperService,
ILogger<NonAnonymousSendCommand> 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);
}
}

View File

@@ -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<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[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<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[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<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[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<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[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<Send>(), Arg.Any<string>());
}
[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<BadRequestException>(
() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
Assert.NotNull(result);
var objectResult = Assert.IsType<ObjectResult>(result);
var response = Assert.IsType<SendFileDownloadDataResponseModel>(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<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}

View File

@@ -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<ISendRepository>();
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
_featureService = Substitute.For<IFeatureService>();
_sendValidationService = Substitute.For<ISendValidationService>();
_currentContext = Substitute.For<ICurrentContext>();
@@ -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<BadRequestException>(() =>
_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<Send>(), Arg.Any<string>());
}
[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<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[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<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[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<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[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<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[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);
}
}