using System.Security.Claims; using System.Text.Json; using AutoFixture.Xunit2; using Bit.Api.Models.Response; using Bit.Api.Tools.Controllers; using Bit.Api.Tools.Models; using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Services; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; namespace Bit.Api.Test.Tools.Controllers; public class SendsControllerTests : IDisposable { private readonly SendsController _sut; private readonly IUserService _userService; private readonly ISendRepository _sendRepository; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; private readonly IAnonymousSendCommand _anonymousSendCommand; private readonly ISendOwnerQuery _sendOwnerQuery; private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; private readonly IFeatureService _featureService; private readonly IPushNotificationService _pushNotificationService; public SendsControllerTests() { _userService = Substitute.For(); _sendRepository = Substitute.For(); _nonAnonymousSendCommand = Substitute.For(); _anonymousSendCommand = Substitute.For(); _sendOwnerQuery = Substitute.For(); _sendAuthorizationService = Substitute.For(); _sendFileStorageService = Substitute.For(); _logger = Substitute.For>(); _featureService = Substitute.For(); _pushNotificationService = Substitute.For(); _sut = new SendsController( _sendRepository, _userService, _sendAuthorizationService, _anonymousSendCommand, _nonAnonymousSendCommand, _sendOwnerQuery, _sendFileStorageService, _logger, _featureService, _pushNotificationService ); } public void Dispose() { _sut?.Dispose(); } [Theory, AutoData] public async Task SendsController_WhenSendHidesEmail_CreatorIdentifierShouldBeNull( Guid id, Send send, User user) { var accessId = CoreHelpers.Base64UrlEncode(id.ToByteArray()); send.Id = default; send.Type = SendType.Text; send.Data = JsonSerializer.Serialize(new Dictionary()); send.HideEmail = true; _sendRepository.GetByIdAsync(Arg.Any()).Returns(send); _sendAuthorizationService.AccessAsync(send, null).Returns(SendAccessResult.Granted); _userService.GetUserByIdAsync(Arg.Any()).Returns(user); var request = new SendAccessRequestModel(); var actionResult = await _sut.Access(accessId, request); var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel; Assert.NotNull(response); Assert.Null(response.CreatorIdentifier); } [Fact] public async Task Post_DeletionDateIsMoreThan31DaysFromNow_ThrowsBadRequest() { var now = DateTime.UtcNow; var expected = "You cannot have a Send with a deletion date that far " + "into the future. Adjust the Deletion Date to a value less than 31 days from now " + "and try again."; var request = new SendRequestModel() { DeletionDate = now.AddDays(32) }; var exception = await Assert.ThrowsAsync(() => _sut.Post(request)); Assert.Equal(expected, exception.Message); } [Fact] public async Task PostFile_DeletionDateIsMoreThan31DaysFromNow_ThrowsBadRequest() { var now = DateTime.UtcNow; var expected = "You cannot have a Send with a deletion date that far " + "into the future. Adjust the Deletion Date to a value less than 31 days from now " + "and try again."; var request = new SendRequestModel() { Type = SendType.File, FileLength = 1024L, DeletionDate = now.AddDays(32) }; var exception = await Assert.ThrowsAsync(() => _sut.PostFile(request)); Assert.Equal(expected, exception.Message); } [Theory, AutoData] public async Task Get_WithValidId_ReturnsSendResponseModel(Guid sendId, Send send) { send.Type = SendType.Text; var textData = new SendTextData("Test Send", "Notes", "Sample text", false); send.Data = JsonSerializer.Serialize(textData); _sendOwnerQuery.Get(sendId, Arg.Any()).Returns(send); var result = await _sut.Get(sendId.ToString()); Assert.NotNull(result); Assert.IsType(result); Assert.Equal(send.Id, result.Id); await _sendOwnerQuery.Received(1).Get(sendId, Arg.Any()); } [Theory, AutoData] public async Task Get_WithInvalidGuid_ThrowsException(string invalidId) { await Assert.ThrowsAsync(() => _sut.Get(invalidId)); } [Fact] public async Task GetAllOwned_ReturnsListResponseModelWithSendResponseModels() { var textSendData = new SendTextData("Test Send 1", "Notes 1", "Sample text", false); var fileSendData = new SendFileData("Test Send 2", "Notes 2", "test.txt") { Id = "file-123", Size = 1024 }; var sends = new List { new Send { Id = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(textSendData) }, new Send { Id = Guid.NewGuid(), Type = SendType.File, Data = JsonSerializer.Serialize(fileSendData) } }; _sendOwnerQuery.GetOwned(Arg.Any()).Returns(sends); var result = await _sut.GetAll(); Assert.NotNull(result); Assert.IsType>(result); Assert.Equal(2, result.Data.Count()); var sendResponseModels = result.Data.ToList(); Assert.Equal(sends[0].Id, sendResponseModels[0].Id); Assert.Equal(sends[1].Id, sendResponseModels[1].Id); await _sendOwnerQuery.Received(1).GetOwned(Arg.Any()); } [Fact] public async Task GetAllOwned_WhenNoSends_ReturnsEmptyListResponseModel() { _sendOwnerQuery.GetOwned(Arg.Any()).Returns(new List()); var result = await _sut.GetAll(); Assert.NotNull(result); Assert.IsType>(result); Assert.Empty(result.Data); await _sendOwnerQuery.Received(1).GetOwned(Arg.Any()); } [Theory, AutoData] public async Task Post_WithPassword_InfersAuthTypePassword(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var request = new SendRequestModel { Type = SendType.Text, Key = "key", Text = new SendTextModel { Text = "text" }, Password = "password", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Post(request); Assert.NotNull(result); Assert.Equal(AuthType.Password, result.AuthType); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.AuthType == AuthType.Password && s.Password != null && s.Emails == null && s.UserId == userId && s.Type == SendType.Text)); _userService.Received(1).GetProperUserId(Arg.Any()); } [Theory, AutoData] public async Task Post_WithEmails_InfersAuthTypeEmail(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var request = new SendRequestModel { Type = SendType.Text, Key = "key", Text = new SendTextModel { Text = "text" }, Emails = "test@example.com", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Post(request); Assert.NotNull(result); Assert.Equal(AuthType.Email, result.AuthType); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.AuthType == AuthType.Email && s.Emails != null && s.Password == null && s.UserId == userId && s.Type == SendType.Text)); _userService.Received(1).GetProperUserId(Arg.Any()); } [Theory, AutoData] public async Task Post_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var request = new SendRequestModel { Type = SendType.Text, Key = "key", Text = new SendTextModel { Text = "text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Post(request); Assert.NotNull(result); Assert.Equal(AuthType.None, result.AuthType); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.AuthType == AuthType.None && s.Password == null && s.Emails == null && s.UserId == userId && s.Type == SendType.Text)); _userService.Received(1).GetProperUserId(Arg.Any()); } [Theory] [InlineData(AuthType.Password)] [InlineData(AuthType.Email)] [InlineData(AuthType.None)] public async Task Access_ReturnsCorrectAuthType(AuthType authType) { var sendId = Guid.NewGuid(); var accessId = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); var send = new Send { Id = sendId, Type = SendType.Text, Data = JsonSerializer.Serialize(new Dictionary()), AuthType = authType }; _sendRepository.GetByIdAsync(sendId).Returns(send); _sendAuthorizationService.AccessAsync(send, "pwd123").Returns(SendAccessResult.Granted); var request = new SendAccessRequestModel(); var actionResult = await _sut.Access(accessId, request); var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel; Assert.NotNull(response); Assert.Equal(authType, response.AuthType); } [Theory] [InlineData(AuthType.Password)] [InlineData(AuthType.Email)] [InlineData(AuthType.None)] public async Task Get_ReturnsCorrectAuthType(AuthType authType) { var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("a", "b", "c", false)), AuthType = authType }; _sendOwnerQuery.Get(sendId, Arg.Any()).Returns(send); var result = await _sut.Get(sendId.ToString()); Assert.NotNull(result); Assert.Equal(authType, result.AuthType); } [Theory, AutoData] public async Task Put_WithValidSend_UpdatesSuccessfully(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), AuthType = AuthType.None }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, Password = "new-password", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId)); } [Theory, AutoData] public async Task Put_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); var request = new SendRequestModel { Type = SendType.Text, Key = "key", Text = new SendTextModel { Text = "text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; await Assert.ThrowsAsync(() => _sut.Put(sendId.ToString(), request)); } [Theory, AutoData] public async Task Put_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = otherUserId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)) }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "key", Text = new SendTextModel { Text = "text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; await Assert.ThrowsAsync(() => _sut.Put(sendId.ToString(), request)); } [Theory, AutoData] public async Task PutRemovePassword_WithValidSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password", AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemovePassword(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.Password == null && s.AuthType == AuthType.None)); } [Theory, AutoData] public async Task PutRemovePassword_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); await Assert.ThrowsAsync(() => _sut.PutRemovePassword(sendId.ToString())); } [Theory, AutoData] public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = otherUserId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password" }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); await Assert.ThrowsAsync(() => _sut.PutRemovePassword(sendId.ToString())); } [Theory, AutoData] public async Task Delete_WithValidSend_DeletesSuccessfully(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)) }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); await _sut.Delete(sendId.ToString()); await _nonAnonymousSendCommand.Received(1).DeleteSendAsync(Arg.Is(s => s.Id == sendId)); } [Theory, AutoData] public async Task Delete_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); await Assert.ThrowsAsync(() => _sut.Delete(sendId.ToString())); } [Theory, AutoData] public async Task Delete_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = otherUserId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)) }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); await Assert.ThrowsAsync(() => _sut.Delete(sendId.ToString())); } [Theory, AutoData] public async Task PostFile_WithPassword_InfersAuthTypePassword(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns("https://example.com/upload") .AndDoes(callInfo => { var send = callInfo.ArgAt(0); var data = callInfo.ArgAt(1); send.Data = JsonSerializer.Serialize(data); }); var request = new SendRequestModel { Type = SendType.File, Key = "key", File = new SendFileModel { FileName = "test.txt" }, FileLength = 1024L, Password = "password", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.PostFile(request); Assert.NotNull(result); Assert.NotNull(result.SendResponse); Assert.Equal(AuthType.Password, result.SendResponse.AuthType); await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync( Arg.Is(s => s.AuthType == AuthType.Password && s.Password != null && s.Emails == null && s.UserId == userId), Arg.Any(), 1024L); } [Theory, AutoData] public async Task PostFile_WithEmails_InfersAuthTypeEmail(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns("https://example.com/upload") .AndDoes(callInfo => { var send = callInfo.ArgAt(0); var data = callInfo.ArgAt(1); send.Data = JsonSerializer.Serialize(data); }); var request = new SendRequestModel { Type = SendType.File, Key = "key", File = new SendFileModel { FileName = "test.txt" }, FileLength = 1024L, Emails = "test@example.com", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.PostFile(request); Assert.NotNull(result); Assert.NotNull(result.SendResponse); Assert.Equal(AuthType.Email, result.SendResponse.AuthType); await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync( Arg.Is(s => s.AuthType == AuthType.Email && s.Emails != null && s.Password == null && s.UserId == userId), Arg.Any(), 1024L); } [Theory, AutoData] public async Task PostFile_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns("https://example.com/upload") .AndDoes(callInfo => { var send = callInfo.ArgAt(0); var data = callInfo.ArgAt(1); send.Data = JsonSerializer.Serialize(data); }); var request = new SendRequestModel { Type = SendType.File, Key = "key", File = new SendFileModel { FileName = "test.txt" }, FileLength = 1024L, DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.PostFile(request); Assert.NotNull(result); Assert.NotNull(result.SendResponse); Assert.Equal(AuthType.None, result.SendResponse.AuthType); await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync( Arg.Is(s => s.AuthType == AuthType.None && s.Password == null && s.Emails == null && s.UserId == userId), Arg.Any(), 1024L); } [Theory, AutoData] public async Task Put_ChangingFromPasswordToEmails_UpdatesAuthTypeToEmail(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), Password = "hashed-password", AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, Emails = "new@example.com", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.AuthType == AuthType.Email && s.Emails != null && s.Password == null)); } [Theory, AutoData] public async Task Put_ChangingFromEmailToPassword_UpdatesAuthTypeToPassword(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), Emails = "old@example.com", AuthType = AuthType.Email }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, Password = "new-password", DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.AuthType == AuthType.Password && s.Password != null && s.Emails == null)); } [Theory, AutoData] public async Task Put_WithoutPasswordOrEmails_PreservesExistingPassword(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), Password = "hashed-password", AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.AuthType == AuthType.Password && s.Password == "hashed-password" && s.Emails == null)); } [Theory, AutoData] public async Task Put_WithoutPasswordOrEmails_PreservesExistingEmails(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), Emails = "test@example.com", AuthType = AuthType.Email }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.AuthType == AuthType.Email && s.Emails == "test@example.com" && s.Password == null)); } [Theory, AutoData] public async Task Put_WithoutPasswordOrEmails_PreservesNoneAuthType(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)), Password = null, Emails = null, AuthType = AuthType.None }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var request = new SendRequestModel { Type = SendType.Text, Key = "updated-key", Text = new SendTextModel { Text = "updated text" }, DeletionDate = DateTime.UtcNow.AddDays(7) }; var result = await _sut.Put(sendId.ToString(), request); Assert.NotNull(result); Assert.Equal(sendId, result.Id); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.AuthType == AuthType.None && s.Password == null && s.Emails == null)); } #region Authenticated Access Endpoints [Theory, AutoData] public async Task AccessUsingAuth_WithValidSend_ReturnsSendAccessResponse(Guid sendId, User creator) { var send = new Send { Id = sendId, UserId = creator.Id, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), HideEmail = false, 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); _userService.GetUserByIdAsync(creator.Id).Returns(creator); var result = await _sut.AccessUsingAuth(); Assert.NotNull(result); var objectResult = Assert.IsType(result); var response = Assert.IsType(objectResult.Value); Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); Assert.Equal(creator.Email, response.CreatorIdentifier); await _sendRepository.Received(1).GetByIdAsync(sendId); await _userService.Received(1).GetUserByIdAsync(creator.Id); } [Theory, AutoData] public async Task AccessUsingAuth_WithHideEmail_DoesNotIncludeCreatorIdentifier(Guid sendId, User creator) { var send = new Send { Id = sendId, UserId = creator.Id, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), HideEmail = true, 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); var result = await _sut.AccessUsingAuth(); Assert.NotNull(result); var objectResult = Assert.IsType(result); var response = Assert.IsType(objectResult.Value); Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); Assert.Null(response.CreatorIdentifier); await _sendRepository.Received(1).GetByIdAsync(sendId); await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); } [Theory, AutoData] public async Task AccessUsingAuth_WithNoUserId_DoesNotIncludeCreatorIdentifier(Guid sendId) { var send = new Send { Id = sendId, UserId = null, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), HideEmail = false, 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); var result = await _sut.AccessUsingAuth(); Assert.NotNull(result); var objectResult = Assert.IsType(result); var response = Assert.IsType(objectResult.Value); Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); Assert.Null(response.CreatorIdentifier); await _sendRepository.Received(1).GetByIdAsync(sendId); await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any()); } [Theory, AutoData] public async Task AccessUsingAuth_WithNonExistentSend_ThrowsBadRequestException(Guid sendId) { var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); var exception = await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); Assert.Equal("Could not locate send", exception.Message); await _sendRepository.Received(1).GetByIdAsync(sendId); } [Theory, AutoData] public async Task AccessUsingAuth_WithFileSend_ReturnsCorrectResponse(Guid sendId, User creator) { var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = "file-123", Size = 2048 }; var send = new Send { Id = sendId, UserId = creator.Id, Type = SendType.File, Data = JsonSerializer.Serialize(fileData), HideEmail = false, 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); _userService.GetUserByIdAsync(creator.Id).Returns(creator); var result = await _sut.AccessUsingAuth(); Assert.NotNull(result); var objectResult = Assert.IsType(result); var response = Assert.IsType(objectResult.Value); Assert.Equal(CoreHelpers.Base64UrlEncode(sendId.ToByteArray()), response.Id); Assert.Equal(SendType.File, response.Type); Assert.NotNull(response.File); Assert.Equal("file-123", response.File.Id); Assert.Equal(creator.Email, response.CreatorIdentifier); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithValidFileId_ReturnsDownloadUrl( Guid sendId, string fileId, string expectedUrl) { 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); _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); var result = await _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); await _sendRepository.Received(1).GetByIdAsync(sendId); await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithNonExistentSend_ThrowsBadRequestException( Guid sendId, string fileId) { var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); var exception = await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); Assert.Equal("Could not locate send", exception.Message); await _sendRepository.Received(1).GetByIdAsync(sendId); await _sendFileStorageService.DidNotReceive() .GetSendFileDownloadUrlAsync(Arg.Any(), Arg.Any()); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_StillReturnsResponse( Guid sendId, string fileId, string expectedUrl) { 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 = 0, MaxAccessCount = null }; var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl); var result = await _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); } #region AccessUsingAuth Validation Tests [Theory, AutoData] public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId) { var send = new Send { Id = sendId, UserId = Guid.NewGuid(), 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); } [Theory, AutoData] public async Task AccessUsingAuth_WithDeletedSend_ThrowsNotFoundException(Guid sendId) { var send = new Send { Id = sendId, UserId = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), DeletionDate = DateTime.UtcNow.AddDays(-1), // Should have been deleted yesterday 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); } [Theory, AutoData] public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId) { var send = new Send { Id = sendId, UserId = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null, Disabled = true, // Disabled 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); } [Theory, AutoData] public async Task AccessUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException(Guid sendId) { var send = new Send { Id = sendId, UserId = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null, Disabled = false, AccessCount = 5, MaxAccessCount = 5 // Limit reached }; var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); await Assert.ThrowsAsync(() => _sut.AccessUsingAuth()); await _sendRepository.Received(1).GetByIdAsync(sendId); } #endregion #region GetSendFileDownloadDataUsingAuth Validation Tests [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithExpiredSend_ThrowsNotFoundException( Guid sendId, string fileId) { var send = new Send { Id = sendId, Type = SendType.File, Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired Disabled = false, AccessCount = 0, MaxAccessCount = null }; var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); await _sendRepository.Received(1).GetByIdAsync(sendId); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithDeletedSend_ThrowsNotFoundException( Guid sendId, string fileId) { var send = new Send { Id = sendId, Type = SendType.File, Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), DeletionDate = DateTime.UtcNow.AddDays(-1), // Deleted 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.GetSendFileDownloadDataUsingAuth(fileId)); await _sendRepository.Received(1).GetByIdAsync(sendId); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithDisabledSend_ThrowsNotFoundException( Guid sendId, string fileId) { var send = new Send { Id = sendId, Type = SendType.File, Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null, Disabled = true, // Disabled AccessCount = 0, MaxAccessCount = null }; var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); await _sendRepository.Received(1).GetByIdAsync(sendId); } [Theory, AutoData] public async Task GetSendFileDownloadDataUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException( Guid sendId, string fileId) { var send = new Send { Id = sendId, Type = SendType.File, Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")), DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null, Disabled = false, AccessCount = 10, MaxAccessCount = 10 // Limit reached }; var user = CreateUserWithSendIdClaim(sendId); _sut.ControllerContext = CreateControllerContextWithUser(user); _sendRepository.GetByIdAsync(sendId).Returns(send); await Assert.ThrowsAsync(() => _sut.GetSendFileDownloadDataUsingAuth(fileId)); await _sendRepository.Received(1).GetByIdAsync(sendId); } #endregion #endregion #region PutRemoveAuth Tests [Theory, AutoData] public async Task PutRemoveAuth_WithPasswordProtectedSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password", Emails = null, AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); Assert.Null(result.Password); Assert.Null(result.Emails); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.Password == null && s.Emails == null && s.AuthType == AuthType.None)); } [Theory, AutoData] public async Task PutRemoveAuth_WithEmailProtectedSend_RemovesEmailsAndSetsAuthTypeNone(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = null, Emails = "test@example.com,user@example.com", AuthType = AuthType.Email }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); Assert.Null(result.Password); Assert.Null(result.Emails); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.Password == null && s.Emails == null && s.AuthType == AuthType.None)); } [Theory, AutoData] public async Task PutRemoveAuth_WithSendAlreadyHavingNoAuth_StillSucceeds(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = null, Emails = null, AuthType = AuthType.None }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); Assert.Null(result.Password); Assert.Null(result.Emails); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.Password == null && s.Emails == null && s.AuthType == AuthType.None)); } [Theory, AutoData] public async Task PutRemoveAuth_WithFileSend_RemovesAuthAndPreservesFileData(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = "file-123", Size = 2048 }; var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.File, Data = JsonSerializer.Serialize(fileData), Password = "hashed-password", Emails = null, AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); Assert.Equal(SendType.File, result.Type); Assert.NotNull(result.File); Assert.Equal("file-123", result.File.Id); Assert.Null(result.Password); Assert.Null(result.Emails); } [Theory, AutoData] public async Task PutRemoveAuth_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); _sendRepository.GetByIdAsync(sendId).Returns((Send)null); await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); await _sendRepository.Received(1).GetByIdAsync(sendId); await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); } [Theory, AutoData] public async Task PutRemoveAuth_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = otherUserId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password", AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); await _sendRepository.Received(1).GetByIdAsync(sendId); await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); } [Theory, AutoData] public async Task PutRemoveAuth_WithNullUserId_ThrowsInvalidOperationException(Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns((Guid?)null); var exception = await Assert.ThrowsAsync(() => _sut.PutRemoveAuth(sendId.ToString())); Assert.Equal("User ID not found", exception.Message); await _sendRepository.DidNotReceive().GetByIdAsync(Arg.Any()); await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); } [Theory, AutoData] public async Task PutRemoveAuth_WithSendHavingBothPasswordAndEmails_RemovesBoth(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password", Emails = "test@example.com", AuthType = AuthType.Password }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); Assert.Null(result.Password); Assert.Null(result.Emails); await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => s.Id == sendId && s.Password == null && s.Emails == null && s.AuthType == AuthType.None)); } [Theory, AutoData] public async Task PutRemoveAuth_PreservesOtherSendProperties(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var deletionDate = DateTime.UtcNow.AddDays(7); var expirationDate = DateTime.UtcNow.AddDays(3); var existingSend = new Send { Id = sendId, UserId = userId, Type = SendType.Text, Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)), Password = "hashed-password", AuthType = AuthType.Password, Key = "encryption-key", MaxAccessCount = 10, AccessCount = 3, DeletionDate = deletionDate, ExpirationDate = expirationDate, Disabled = false, HideEmail = true }; _sendRepository.GetByIdAsync(sendId).Returns(existingSend); var result = await _sut.PutRemoveAuth(sendId.ToString()); Assert.NotNull(result); Assert.Equal(sendId, result.Id); Assert.Equal(AuthType.None, result.AuthType); // Verify other properties are preserved Assert.Equal("encryption-key", result.Key); Assert.Equal(10, result.MaxAccessCount); Assert.Equal(3, result.AccessCount); Assert.Equal(deletionDate, result.DeletionDate); Assert.Equal(expirationDate, result.ExpirationDate); Assert.False(result.Disabled); Assert.True(result.HideEmail); } #endregion #region Test Helpers private static ClaimsPrincipal CreateUserWithSendIdClaim(Guid sendId) { var claims = new List { new Claim("send_id", sendId.ToString()) }; var identity = new ClaimsIdentity(claims, "TestAuth"); return new ClaimsPrincipal(identity); } private static ControllerContext CreateControllerContextWithUser(ClaimsPrincipal user) { return new ControllerContext { HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext { User = user } }; } #endregion }