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; using NSubstitute; using Xunit; namespace Bit.Core.Test.Tools.Services; public class SendAuthenticationQueryTests { private readonly ISendRepository _sendRepository; private readonly SendAuthenticationQuery _sendAuthenticationQuery; public SendAuthenticationQueryTests() { _sendRepository = Substitute.For(); _sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository); } [Fact] public void Constructor_WithNullRepository_ThrowsArgumentNullException() { // Act & Assert var exception = Assert.Throws(() => new SendAuthenticationQuery(null)); Assert.Equal("sendRepository", exception.ParamName); } [Theory] [MemberData(nameof(AuthenticationMethodTestCases))] public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType) { // Arrange var sendId = Guid.NewGuid(); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(expectedType, result); } [Theory] [MemberData(nameof(EmailHashesParsingTestCases))] public async Task GetAuthenticationMethod_WithEmailHashes_ParsesEmailHashesCorrectly(string emailHashString, string[] expectedEmailHashes) { // Arrange var sendId = Guid.NewGuid(); var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: emailHashString, password: null, AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert var emailOtp = Assert.IsType(result); Assert.Equal(expectedEmailHashes, emailOtp.Emails); } [Fact] public async Task GetAuthenticationMethod_WithBothEmailHashesAndPassword_ReturnsEmailOtp() { // Arrange var sendId = Guid.NewGuid(); var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: "hashedemail", password: "hashedpassword", AuthType.Email); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId() { // Arrange var sendId = Guid.NewGuid(); var send = CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: null, AuthType.None); _sendRepository.GetByIdAsync(sendId).Returns(send); // Act await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert await _sendRepository.Received(1).GetByIdAsync(sendId); } [Fact] public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException() { // Arrange var sendId = Guid.NewGuid(); var expectedException = new InvalidOperationException("Repository error"); _sendRepository.GetByIdAsync(sendId).Returns(Task.FromException(expectedException)); // Act & Assert var exception = await Assert.ThrowsAsync(() => _sendAuthenticationQuery.GetAuthenticationMethod(sendId)); Assert.Same(expectedException, exception); } public static IEnumerable AuthenticationMethodTestCases() { yield return new object[] { null, typeof(NeverAuthenticate) }; yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emailHashes: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emailHashes: null, password: null, AuthType.None), typeof(NeverAuthenticate) }; yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: "hashedemail", password: null, AuthType.Email), typeof(EmailOtp) }; yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: "hashedpassword", AuthType.Password), typeof(ResourcePassword) }; yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emailHashes: null, password: null, AuthType.None), typeof(NotAuthenticated) }; } [Fact] public async Task GetAuthenticationMethod_WithDisabledSend_ReturnsNeverAuthenticate() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 0, MaxAccessCount = 10, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = true, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithExpiredSend_ReturnsNeverAuthenticate() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 0, MaxAccessCount = 10, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = DateTime.UtcNow.AddDays(-1) // Expired yesterday }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithDeletionDatePassed_ReturnsNeverAuthenticate() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 0, MaxAccessCount = 10, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(-1), // Should have been deleted yesterday ExpirationDate = null }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithDeletionDateEqualToNow_ReturnsNeverAuthenticate() { // Arrange var sendId = Guid.NewGuid(); var now = DateTime.UtcNow; var send = new Send { Id = sendId, AccessCount = 0, MaxAccessCount = 10, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = now, // DeletionDate <= DateTime.UtcNow ExpirationDate = null }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithAccessCountEqualToMaxAccessCount_ReturnsNeverAuthenticate() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 5, MaxAccessCount = 5, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithNullMaxAccessCount_DoesNotRestrictAccess() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 1000, MaxAccessCount = null, // No limit EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } [Fact] public async Task GetAuthenticationMethod_WithNullExpirationDate_DoesNotExpire() { // Arrange var sendId = Guid.NewGuid(); var send = new Send { Id = sendId, AccessCount = 0, MaxAccessCount = 10, EmailHashes = "hashedemail", Password = null, AuthType = AuthType.Email, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null // No expiration }; _sendRepository.GetByIdAsync(sendId).Returns(send); // Act var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); // Assert Assert.IsType(result); } public static IEnumerable EmailHashesParsingTestCases() { yield return new object[] { "hash1", new[] { "hash1" } }; yield return new object[] { "hash1,hash2", new[] { "hash1", "hash2" } }; yield return new object[] { " hash1 , hash2 ", new[] { "hash1", "hash2" } }; yield return new object[] { "hash1,,hash2", new[] { "hash1", "hash2" } }; yield return new object[] { " , hash1, ,hash2, ", new[] { "hash1", "hash2" } }; } private static Send CreateSend(int accessCount, int? maxAccessCount, string? emailHashes, string? password, AuthType? authType) { return new Send { Id = Guid.NewGuid(), AccessCount = accessCount, MaxAccessCount = maxAccessCount, EmailHashes = emailHashes, Password = password, AuthType = authType, Disabled = false, DeletionDate = DateTime.UtcNow.AddDays(7), ExpirationDate = null }; } }