1
0
mirror of https://github.com/bitwarden/server synced 2026-01-29 07:43:22 +00:00
Files
server/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
John Harrington fa06fe41ab [PM-30920] Server changes to encrypt send access email list (#6867)
* models, entity, and stored procs updated to work with EmailHashes with migrations

* configure data protection for EmailHashes

* update SendAuthenticationQuery to use EmailHashes and perform validation

* respond to Claude's comments and update tests

* fix send.sql alignment

Co-authored-by: mkincaid-bw <mkincaid@bitwarden.com>

---------

Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com>
Co-authored-by: mkincaid-bw <mkincaid@bitwarden.com>
2026-01-28 07:13:25 -07:00

324 lines
11 KiB
C#

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<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]
[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<EmailOtp>(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<EmailOtp>(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<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) };
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<NeverAuthenticate>(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<NeverAuthenticate>(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<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,
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<NeverAuthenticate>(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<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
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<EmailOtp>(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<EmailOtp>(result);
}
public static IEnumerable<object[]> 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
};
}
}