2025-05-27 08:25:27 -04:00
|
|
|
|
using Bit.Core.Tools.Entities;
|
2026-01-14 14:07:46 -07:00
|
|
|
|
using Bit.Core.Tools.Enums;
|
2025-05-27 08:25:27 -04:00
|
|
|
|
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<ISendRepository>();
|
|
|
|
|
|
_sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public void Constructor_WithNullRepository_ThrowsArgumentNullException()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Act & Assert
|
|
|
|
|
|
var exception = Assert.Throws<ArgumentNullException>(() => 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]
|
2026-02-09 12:58:57 -08:00
|
|
|
|
[MemberData(nameof(EmailsParsingTestCases))]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)
|
2025-05-27 08:25:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
2026-02-09 12:58:57 -08:00
|
|
|
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null, AuthType.Email);
|
2025-05-27 08:25:27 -04:00
|
|
|
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
var emailOtp = Assert.IsType<EmailOtp>(result);
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Assert.Equal(expectedEmails, emailOtp.emails);
|
2025-05-27 08:25:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-09 12:58:57 -08:00
|
|
|
|
public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()
|
2025-05-27 08:25:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
2026-02-09 12:58:57 -08:00
|
|
|
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "person@company.com", password: "hashedpassword", AuthType.Email);
|
2025-05-27 08:25:27 -04:00
|
|
|
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
|
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
|
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
|
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
|
Assert.IsType<EmailOtp>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
2026-02-09 12:58:57 -08:00
|
|
|
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null, AuthType.None);
|
2025-05-27 08:25:27 -04:00
|
|
|
|
_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<Send?>(expectedException));
|
|
|
|
|
|
|
|
|
|
|
|
// Act & Assert
|
|
|
|
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
|
|
|
|
_sendAuthenticationQuery.GetAuthenticationMethod(sendId));
|
|
|
|
|
|
Assert.Same(expectedException, exception);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> AuthenticationMethodTestCases()
|
|
|
|
|
|
{
|
|
|
|
|
|
yield return new object[] { null, typeof(NeverAuthenticate) };
|
2026-02-09 12:58:57 -08:00
|
|
|
|
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: "person@company.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) };
|
2025-05-27 08:25:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-28 07:13:25 -07:00
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithDisabledSend_ReturnsNeverAuthenticate()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
|
|
|
|
|
var send = new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = sendId,
|
|
|
|
|
|
AccessCount = 0,
|
|
|
|
|
|
MaxAccessCount = 10,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<NeverAuthenticate>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithExpiredSend_ReturnsNeverAuthenticate()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
|
|
|
|
|
var send = new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = sendId,
|
|
|
|
|
|
AccessCount = 0,
|
|
|
|
|
|
MaxAccessCount = 10,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<NeverAuthenticate>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithDeletionDatePassed_ReturnsNeverAuthenticate()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
|
|
|
|
|
var send = new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = sendId,
|
|
|
|
|
|
AccessCount = 0,
|
|
|
|
|
|
MaxAccessCount = 10,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<NeverAuthenticate>(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,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<NeverAuthenticate>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithAccessCountEqualToMaxAccessCount_ReturnsNeverAuthenticate()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
|
|
|
|
|
var send = new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = sendId,
|
|
|
|
|
|
AccessCount = 5,
|
|
|
|
|
|
MaxAccessCount = 5,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<NeverAuthenticate>(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
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<EmailOtp>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
|
public async Task GetAuthenticationMethod_WithNullExpirationDate_DoesNotExpire()
|
|
|
|
|
|
{
|
|
|
|
|
|
// Arrange
|
|
|
|
|
|
var sendId = Guid.NewGuid();
|
|
|
|
|
|
var send = new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = sendId,
|
|
|
|
|
|
AccessCount = 0,
|
|
|
|
|
|
MaxAccessCount = 10,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = "person@company.com",
|
2026-01-28 07:13:25 -07:00
|
|
|
|
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<EmailOtp>(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 12:58:57 -08:00
|
|
|
|
public static IEnumerable<object[]> EmailsParsingTestCases()
|
2025-05-27 08:25:27 -04:00
|
|
|
|
{
|
2026-02-09 12:58:57 -08:00
|
|
|
|
yield return new object[] { "person@company.com", new[] { "person@company.com" } };
|
|
|
|
|
|
yield return new object[] { "person1@company.com,person2@company.com", new[] { "person1@company.com", "person2@company.com" } };
|
|
|
|
|
|
yield return new object[] { " person1@company.com , person2@company.com ", new[] { "person1@company.com", "person2@company.com" } };
|
|
|
|
|
|
yield return new object[] { "person1@company.com,,person2@company.com", new[] { "person1@company.com", "person2@company.com" } };
|
|
|
|
|
|
yield return new object[] { " , person1@company.com, ,person2@company.com, ", new[] { "person1@company.com", "person2@company.com" } };
|
2025-05-27 08:25:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 12:58:57 -08:00
|
|
|
|
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password, AuthType? authType)
|
2025-05-27 08:25:27 -04:00
|
|
|
|
{
|
|
|
|
|
|
return new Send
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
|
AccessCount = accessCount,
|
|
|
|
|
|
MaxAccessCount = maxAccessCount,
|
2026-02-09 12:58:57 -08:00
|
|
|
|
Emails = emails,
|
2026-01-14 14:07:46 -07:00
|
|
|
|
Password = password,
|
2026-01-28 07:13:25 -07:00
|
|
|
|
AuthType = authType,
|
|
|
|
|
|
Disabled = false,
|
|
|
|
|
|
DeletionDate = DateTime.UtcNow.AddDays(7),
|
|
|
|
|
|
ExpirationDate = null
|
2025-05-27 08:25:27 -04:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|