diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 449d9573fd82..f9f71d076dcb 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -5,9 +5,11 @@ using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; using Bit.Core; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.UserFeatures.SendAccess; using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; @@ -22,7 +24,6 @@ namespace Bit.Api.Tools.Controllers; [Route("sends")] -[Authorize("Application")] public class SendsController : Controller { private readonly ISendRepository _sendRepository; @@ -31,11 +32,10 @@ public class SendsController : Controller private readonly ISendFileStorageService _sendFileStorageService; private readonly IAnonymousSendCommand _anonymousSendCommand; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; - private readonly ISendOwnerQuery _sendOwnerQuery; - private readonly ILogger _logger; - private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; + private readonly IPushNotificationService _pushNotificationService; public SendsController( ISendRepository sendRepository, @@ -46,7 +46,8 @@ public SendsController( ISendOwnerQuery sendOwnerQuery, ISendFileStorageService sendFileStorageService, ILogger logger, - GlobalSettings globalSettings) + IFeatureService featureService, + IPushNotificationService pushNotificationService) { _sendRepository = sendRepository; _userService = userService; @@ -56,10 +57,12 @@ public SendsController( _sendOwnerQuery = sendOwnerQuery; _sendFileStorageService = sendFileStorageService; _logger = logger; - _globalSettings = globalSettings; + _featureService = featureService; + _pushNotificationService = pushNotificationService; } #region Anonymous endpoints + [AllowAnonymous] [HttpPost("access/{id}")] public async Task Access(string id, [FromBody] SendAccessRequestModel model) @@ -73,21 +76,32 @@ public async Task Access(string id, [FromBody] SendAccessRequestM var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); var send = await _sendRepository.GetByIdAsync(guid); + if (send == null) { throw new BadRequestException("Could not locate send"); } + + /* This guard can be removed once feature flag is retired*/ + var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP); + if (sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null) + { + return new UnauthorizedResult(); + } + var sendAuthResult = await _sendAuthorizationService.AccessAsync(send, model.Password); if (sendAuthResult.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } + if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } + if (sendAuthResult.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); @@ -99,6 +113,7 @@ public async Task Access(string id, [FromBody] SendAccessRequestM var creator = await _userService.GetUserByIdAsync(send.UserId.Value); sendResponse.CreatorIdentifier = creator.Email; } + return new ObjectResult(sendResponse); } @@ -122,6 +137,13 @@ public async Task GetSendFileDownloadData(string encodedSendId, throw new BadRequestException("Could not locate send"); } + /* This guard can be removed once feature flag is retired*/ + var sendEmailOtpEnabled = _featureService.IsEnabled(FeatureFlagKeys.SendEmailOTP); + if (sendEmailOtpEnabled && send.AuthType == AuthType.Email && send.Emails is not null) + { + return new UnauthorizedResult(); + } + var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, model.Password); @@ -129,21 +151,19 @@ public async Task GetSendFileDownloadData(string encodedSendId, { return new UnauthorizedResult(); } + if (result.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } + if (result.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } - return new ObjectResult(new SendFileDownloadDataResponseModel() - { - Id = fileId, - Url = url, - }); + return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url, }); } [AllowAnonymous] @@ -157,7 +177,8 @@ public async Task AzureValidateFile() { try { - var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; + var blobName = + eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); if (send == null) @@ -166,6 +187,7 @@ public async Task AzureValidateFile() { await azureSendFileStorageService.DeleteBlobAsync(blobName); } + return; } @@ -173,7 +195,8 @@ public async Task AzureValidateFile() } catch (Exception e) { - _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent)); + _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", + JsonSerializer.Serialize(eventGridEvent)); return; } } @@ -185,6 +208,7 @@ public async Task AzureValidateFile() #region Non-anonymous endpoints + [Authorize(Policies.Application)] [HttpGet("{id}")] public async Task Get(string id) { @@ -193,6 +217,7 @@ public async Task Get(string id) return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpGet("")] public async Task> GetAll() { @@ -203,6 +228,67 @@ public async Task> GetAll() return result; } + [Authorize(Policy = Policies.Send)] + // [RequireFeature(FeatureFlagKeys.SendEmailOTP)] /* Uncomment once client fallback re-try logic is added */ + [HttpPost("access/")] + public async Task AccessUsingAuth() + { + var guid = User.GetSendId(); + var send = await _sendRepository.GetByIdAsync(guid); + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } + if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled || + send.DeletionDate < DateTime.UtcNow) + { + throw new NotFoundException(); + } + + var sendResponse = new SendAccessResponseModel(send); + if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault()) + { + var creator = await _userService.GetUserByIdAsync(send.UserId.Value); + sendResponse.CreatorIdentifier = creator.Email; + } + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + + return new ObjectResult(sendResponse); + } + + [Authorize(Policy = Policies.Send)] + // [RequireFeature(FeatureFlagKeys.SendEmailOTP)] /* Uncomment once client fallback re-try logic is added */ + [HttpPost("access/file/{fileId}")] + public async Task GetSendFileDownloadDataUsingAuth(string fileId) + { + var sendId = User.GetSendId(); + var send = await _sendRepository.GetByIdAsync(sendId); + + if (send == null) + { + throw new BadRequestException("Could not locate send"); + } + if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled || + send.DeletionDate < DateTime.UtcNow) + { + throw new NotFoundException(); + } + + var url = await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId); + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + + return new ObjectResult(new SendFileDownloadDataResponseModel() { Id = fileId, Url = url }); + } + + [Authorize(Policies.Application)] [HttpPost("")] public async Task Post([FromBody] SendRequestModel model) { @@ -213,6 +299,7 @@ public async Task Post([FromBody] SendRequestModel model) return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpPost("file/v2")] public async Task PostFile([FromBody] SendRequestModel model) { @@ -243,6 +330,7 @@ public async Task PostFile([FromBody] SendReque }; } + [Authorize(Policies.Application)] [HttpGet("{id}/file/{fileId}")] public async Task RenewFileUpload(string id, string fileId) { @@ -267,6 +355,7 @@ public async Task RenewFileUpload(string id, st }; } + [Authorize(Policies.Application)] [HttpPost("{id}/file/{fileId}")] [SelfHosted(SelfHostedOnly = true)] [RequestSizeLimit(Constants.FileSize501mb)] @@ -283,12 +372,14 @@ public async Task PostFileForExistingSend(string id, string fileId) { throw new BadRequestException("Could not locate send"); } + await Request.GetFileAsync(async (stream) => { await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); }); } + [Authorize(Policies.Application)] [HttpPut("{id}")] public async Task Put(string id, [FromBody] SendRequestModel model) { @@ -304,6 +395,7 @@ public async Task Put(string id, [FromBody] SendRequestModel return new SendResponseModel(send); } + [Authorize(Policies.Application)] [HttpPut("{id}/remove-password")] public async Task PutRemovePassword(string id) { @@ -322,6 +414,28 @@ public async Task PutRemovePassword(string id) return new SendResponseModel(send); } + // Removes ALL authentication (email or password) if any is present + [Authorize(Policies.Application)] + [HttpPut("{id}/remove-auth")] + public async Task PutRemoveAuth(string id) + { + var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); + var send = await _sendRepository.GetByIdAsync(new Guid(id)); + if (send == null || send.UserId != userId) + { + throw new NotFoundException(); + } + + // This endpoint exists because PUT preserves existing Password/Emails when not provided. + // This allows clients to update other fields without re-submitting sensitive auth data. + send.Password = null; + send.Emails = null; + send.AuthType = AuthType.None; + await _nonAnonymousSendCommand.SaveSendAsync(send); + return new SendResponseModel(send); + } + + [Authorize(Policies.Application)] [HttpDelete("{id}")] public async Task Delete(string id) { diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a4199522b9f5..120328b9608b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -236,16 +236,6 @@ public static class FeatureFlagKeys public const string SendUIRefresh = "pm-28175-send-ui-refresh"; public const string SendEmailOTP = "pm-19051-send-email-verification"; - /// - /// Enable this flag to output email/OTP authenticated sends from the `GET sends` endpoint. When - /// this flag is disabled, the `GET sends` endpoint omits email/OTP authenticated sends. - /// - /// - /// This flag is server-side only, and only inhibits the endpoint returning all sends. - /// Email/OTP sends can still be created and downloaded through other endpoints. - /// - public const string PM19051_ListEmailOtpSends = "tools-send-email-otp-listing"; - /* Vault Team */ public const string CipherKeyEncryption = "cipher-key-encryption"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; diff --git a/src/Core/Tools/Enums/AuthType.cs b/src/Core/Tools/Enums/AuthType.cs index 814ebf69b87d..4a31275b7a4f 100644 --- a/src/Core/Tools/Enums/AuthType.cs +++ b/src/Core/Tools/Enums/AuthType.cs @@ -1,11 +1,8 @@ -using System.Text.Json.Serialization; - -namespace Bit.Core.Tools.Enums; +namespace Bit.Core.Tools.Enums; /// /// Specifies the authentication method required to access a Send. /// -[JsonConverter(typeof(JsonStringEnumConverter))] public enum AuthType : byte { /// diff --git a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs index fed7c9e8d4eb..97c2e64dc58c 100644 --- a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs +++ b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; @@ -37,8 +38,8 @@ public async Task GetAuthenticationMethod(Guid sendId) { null => NEVER_AUTHENTICATE, var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE, - var s when s.Emails is not null => emailOtp(s.Emails), - var s when s.Password is not null => new ResourcePassword(s.Password), + var s when s.AuthType == AuthType.Email && s.Emails is not null => emailOtp(s.Emails), + var s when s.AuthType == AuthType.Password && s.Password is not null => new ResourcePassword(s.Password), _ => NOT_AUTHENTICATED }; diff --git a/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs index cb539429a570..29bd8f56f96c 100644 --- a/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs +++ b/src/Core/Tools/SendFeatures/Queries/SendOwnerQuery.cs @@ -12,7 +12,6 @@ namespace Bit.Core.Tools.SendFeatures.Queries; public class SendOwnerQuery : ISendOwnerQuery { private readonly ISendRepository _repository; - private readonly IFeatureService _features; private readonly IUserService _users; /// @@ -24,10 +23,9 @@ public class SendOwnerQuery : ISendOwnerQuery /// /// Thrown when is . /// - public SendOwnerQuery(ISendRepository sendRepository, IFeatureService features, IUserService users) + public SendOwnerQuery(ISendRepository sendRepository, IUserService users) { _repository = sendRepository; - _features = features ?? throw new ArgumentNullException(nameof(features)); _users = users ?? throw new ArgumentNullException(nameof(users)); } @@ -51,16 +49,6 @@ public async Task> GetOwned(ClaimsPrincipal user) var userId = _users.GetProperUserId(user) ?? throw new BadRequestException("invalid user."); var sends = await _repository.GetManyByUserIdAsync(userId); - var removeEmailOtp = !_features.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); - if (removeEmailOtp) - { - // reify list to avoid invalidating the enumerator - foreach (var s in sends.Where(s => s.Emails != null).ToList()) - { - sends.Remove(s); - } - } - return sends; } } diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 541e8a4903b7..e3a9ba443576 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -8,8 +8,8 @@ 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.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; @@ -28,7 +28,6 @@ namespace Bit.Api.Test.Tools.Controllers; public class SendsControllerTests : IDisposable { private readonly SendsController _sut; - private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; private readonly ISendRepository _sendRepository; private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; @@ -37,6 +36,8 @@ public class SendsControllerTests : IDisposable private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; + private readonly IFeatureService _featureService; + private readonly IPushNotificationService _pushNotificationService; public SendsControllerTests() { @@ -47,8 +48,9 @@ public SendsControllerTests() _sendOwnerQuery = Substitute.For(); _sendAuthorizationService = Substitute.For(); _sendFileStorageService = Substitute.For(); - _globalSettings = new GlobalSettings(); _logger = Substitute.For>(); + _featureService = Substitute.For(); + _pushNotificationService = Substitute.For(); _sut = new SendsController( _sendRepository, @@ -59,7 +61,8 @@ public SendsControllerTests() _sendOwnerQuery, _sendFileStorageService, _logger, - _globalSettings + _featureService, + _pushNotificationService ); } @@ -96,8 +99,8 @@ 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."; + "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)); @@ -109,9 +112,10 @@ 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) }; + "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); @@ -409,7 +413,8 @@ public async Task PutRemovePassword_WithNonExistentSend_ThrowsNotFoundException( } [Theory, AutoData] - public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId) + public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, + Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); var existingSend = new Send @@ -753,4 +758,683 @@ await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is(s => 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 } diff --git a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs index c34afc42bddb..7901b3c5c0ce 100644 --- a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs +++ b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs @@ -1,4 +1,5 @@ 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.Queries; @@ -47,7 +48,7 @@ public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(strin { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null, AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -63,7 +64,7 @@ public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmail { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword"); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword", AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -78,7 +79,7 @@ public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId() { // Arrange var sendId = Guid.NewGuid(); - var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act @@ -105,11 +106,11 @@ public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesExcepti public static IEnumerable AuthenticationMethodTestCases() { yield return new object[] { null, typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) }; - yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) }; + yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null, AuthType.Email), typeof(EmailOtp) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword", AuthType.Password), typeof(ResourcePassword) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None), typeof(NotAuthenticated) }; } public static IEnumerable EmailParsingTestCases() @@ -121,7 +122,7 @@ public static IEnumerable EmailParsingTestCases() yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } }; } - private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password) + private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password, AuthType? authType) { return new Send { @@ -129,7 +130,8 @@ private static Send CreateSend(int accessCount, int? maxAccessCount, string? ema AccessCount = accessCount, MaxAccessCount = maxAccessCount, Emails = emails, - Password = password + Password = password, + AuthType = authType }; } } diff --git a/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs b/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs index 9a2f942eb892..71ed27f2ac7a 100644 --- a/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs +++ b/test/Core.Test/Tools/Services/SendOwnerQueryTests.cs @@ -12,7 +12,6 @@ namespace Bit.Core.Test.Tools.Services; public class SendOwnerQueryTests { private readonly ISendRepository _sendRepository; - private readonly IFeatureService _featureService; private readonly IUserService _userService; private readonly SendOwnerQuery _sendOwnerQuery; private readonly Guid _currentUserId = Guid.NewGuid(); @@ -21,11 +20,10 @@ public class SendOwnerQueryTests public SendOwnerQueryTests() { _sendRepository = Substitute.For(); - _featureService = Substitute.For(); _userService = Substitute.For(); _user = new ClaimsPrincipal(); _userService.GetProperUserId(_user).Returns(_currentUserId); - _sendOwnerQuery = new SendOwnerQuery(_sendRepository, _featureService, _userService); + _sendOwnerQuery = new SendOwnerQuery(_sendRepository, _userService); } [Fact] @@ -84,7 +82,7 @@ public async Task Get_WithNullCurrentUserId_ThrowsBadRequestException() } [Fact] - public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends() + public async Task GetOwned_ReturnsAllSendsIncludingEmailOTP() { // Arrange var sends = new List @@ -94,7 +92,6 @@ public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends() CreateSend(Guid.NewGuid(), _currentUserId, emails: "other@example.com") }; _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true); // Act var result = await _sendOwnerQuery.GetOwned(_user); @@ -105,28 +102,6 @@ public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends() Assert.Contains(sends[1], result); Assert.Contains(sends[2], result); await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId); - _featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); - } - - [Fact] - public async Task GetOwned_WithFeatureFlagDisabled_FiltersOutEmailOtpSends() - { - // Arrange - var sendWithoutEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: null); - var sendWithEmails = CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com"); - var sends = new List { sendWithoutEmails, sendWithEmails }; - _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(sends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(false); - - // Act - var result = await _sendOwnerQuery.GetOwned(_user); - - // Assert - Assert.Single(result); - Assert.Contains(sendWithoutEmails, result); - Assert.DoesNotContain(sendWithEmails, result); - await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId); - _featureService.Received(1).IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends); } [Fact] @@ -147,7 +122,6 @@ public async Task GetOwned_WithEmptyCollection_ReturnsEmptyCollection() // Arrange var emptySends = new List(); _sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends); - _featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true); // Act var result = await _sendOwnerQuery.GetOwned(_user);