Files
server/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

324 lines
11 KiB
C#
Raw Normal View History

using Bit.Core.Tools.Entities;
[Tools] Update SendAuthenticationQuery, add new non-anonymous endpoints, and add PutRemoveAuth endpoint (#6786) * update send api models to support new `email` field * normalize authentication field evaluation order * document send response converters * add FIXME to remove unused constructor argument * add FIXME to remove unused constructor argument * introduce `tools-send-email-otp-listing` feature flag * add `ISendOwnerQuery` to dependency graph * fix broken tests * added AuthType prop to send related models with test coverage and debt cleanup * dotnet format * add migrations * dotnet format * make SendsController null safe (tech debt) * add AuthType col to Sends table, change Emails col length to 4000, and run migrations * dotnet format * update SPs to expect AuthType * include SP updates in migrations * remove migrations not intended for merge * Revert "remove migrations not intended for merge" This reverts commit 7df56e346ab3402387bebffc1d112d73214632fa. undo migrations removal * extract AuthType inference to util method and remove SQLite file * fix lints * address review comments * fix incorrect assignment and adopt SQL conventions * fix column assignment order in Send_Update.sql * remove space added to email list * assign SQL default value of NULL to AuthType * update SPs to match migration changes * remove FF, update SendAuthQuery, and update tests * new endpoints added but lack test coverage * dotnet format * add PutRemoveAuth endpoint with test coverage and tests for new non-anon endpoints * update RequireFeatureFlag comment for clarity * respond to Claude's findings * add additional validation logic to new auth endpoints * enforce auth policies on individual action methods * remove JsonConverter directive for AuthType * remove tools-send-email-otp-listing feature flag --------- Co-authored-by: ✨ Audrey ✨ <audrey@audreyality.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com>
2026-01-14 14:07:46 -07:00
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,
[Tools] Update SendAuthenticationQuery, add new non-anonymous endpoints, and add PutRemoveAuth endpoint (#6786) * update send api models to support new `email` field * normalize authentication field evaluation order * document send response converters * add FIXME to remove unused constructor argument * add FIXME to remove unused constructor argument * introduce `tools-send-email-otp-listing` feature flag * add `ISendOwnerQuery` to dependency graph * fix broken tests * added AuthType prop to send related models with test coverage and debt cleanup * dotnet format * add migrations * dotnet format * make SendsController null safe (tech debt) * add AuthType col to Sends table, change Emails col length to 4000, and run migrations * dotnet format * update SPs to expect AuthType * include SP updates in migrations * remove migrations not intended for merge * Revert "remove migrations not intended for merge" This reverts commit 7df56e346ab3402387bebffc1d112d73214632fa. undo migrations removal * extract AuthType inference to util method and remove SQLite file * fix lints * address review comments * fix incorrect assignment and adopt SQL conventions * fix column assignment order in Send_Update.sql * remove space added to email list * assign SQL default value of NULL to AuthType * update SPs to match migration changes * remove FF, update SendAuthQuery, and update tests * new endpoints added but lack test coverage * dotnet format * add PutRemoveAuth endpoint with test coverage and tests for new non-anon endpoints * update RequireFeatureFlag comment for clarity * respond to Claude's findings * add additional validation logic to new auth endpoints * enforce auth policies on individual action methods * remove JsonConverter directive for AuthType * remove tools-send-email-otp-listing feature flag --------- Co-authored-by: ✨ Audrey ✨ <audrey@audreyality.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Alex Dragovich <46065570+itsadrago@users.noreply.github.com>
2026-01-14 14:07:46 -07:00
Password = password,
AuthType = authType,
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null
};
}
}