1
0
mirror of https://github.com/bitwarden/server synced 2026-01-17 07:53:36 +00:00

introduce tools-send-email-otp-listing feature flag

This commit is contained in:
✨ Audrey ✨
2025-08-15 15:17:35 -04:00
parent 1e9023da8a
commit 32715566ab
8 changed files with 373 additions and 25 deletions

View File

@@ -1,5 +1,7 @@
using System.Text.Json;
using System.Security.Claims;
using System.Text.Json;
using AutoFixture.Xunit2;
using Bit.Api.Models.Response;
using Bit.Api.Tools.Controllers;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response;
@@ -12,6 +14,7 @@ using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
@@ -29,6 +32,7 @@ public class SendsControllerTests : IDisposable
private readonly ISendRepository _sendRepository;
private readonly INonAnonymousSendCommand _nonAnonymousSendCommand;
private readonly IAnonymousSendCommand _anonymousSendCommand;
private readonly ISendOwnerQuery _sendOwnerQuery;
private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly ILogger<SendsController> _logger;
@@ -39,6 +43,7 @@ public class SendsControllerTests : IDisposable
_sendRepository = Substitute.For<ISendRepository>();
_nonAnonymousSendCommand = Substitute.For<INonAnonymousSendCommand>();
_anonymousSendCommand = Substitute.For<IAnonymousSendCommand>();
_sendOwnerQuery = Substitute.For<ISendOwnerQuery>();
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
_globalSettings = new GlobalSettings();
@@ -50,6 +55,7 @@ public class SendsControllerTests : IDisposable
_sendAuthorizationService,
_anonymousSendCommand,
_nonAnonymousSendCommand,
_sendOwnerQuery,
_sendFileStorageService,
_logger,
_globalSettings
@@ -109,4 +115,62 @@ public class SendsControllerTests : IDisposable
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostFile(request));
Assert.Equal(expected, exception.Message);
}
[Theory, AutoData]
public async Task Get_WithValidId_ReturnsSendResponseModel(Guid sendId, Send send)
{
send.Type = SendType.Text;
var textData = new SendTextData("Test Send", "Notes", "Sample text", false);
send.Data = JsonSerializer.Serialize(textData);
_sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);
var result = await _sut.Get(sendId.ToString());
Assert.NotNull(result);
Assert.IsType<SendResponseModel>(result);
Assert.Equal(send.Id, result.Id);
await _sendOwnerQuery.Received(1).Get(sendId, Arg.Any<ClaimsPrincipal>());
}
[Theory, AutoData]
public async Task Get_WithInvalidGuid_ThrowsException(string invalidId)
{
await Assert.ThrowsAsync<FormatException>(() => _sut.Get(invalidId));
}
[Fact]
public async Task GetAllOwned_ReturnsListResponseModelWithSendResponseModels()
{
var textSendData = new SendTextData("Test Send 1", "Notes 1", "Sample text", false);
var fileSendData = new SendFileData("Test Send 2", "Notes 2", "test.txt") { Id = "file-123", Size = 1024 };
var sends = new List<Send>
{
new Send { Id = Guid.NewGuid(), Type = SendType.Text, Data = JsonSerializer.Serialize(textSendData) },
new Send { Id = Guid.NewGuid(), Type = SendType.File, Data = JsonSerializer.Serialize(fileSendData) }
};
_sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(sends);
var result = await _sut.Get();
Assert.NotNull(result);
Assert.IsType<ListResponseModel<SendResponseModel>>(result);
Assert.Equal(2, result.Data.Count());
var sendResponseModels = result.Data.ToList();
Assert.Equal(sends[0].Id, sendResponseModels[0].Id);
Assert.Equal(sends[1].Id, sendResponseModels[1].Id);
await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());
}
[Fact]
public async Task GetAllOwned_WhenNoSends_ReturnsEmptyListResponseModel()
{
_sendOwnerQuery.GetOwned(Arg.Any<ClaimsPrincipal>()).Returns(new List<Send>());
var result = await _sut.Get();
Assert.NotNull(result);
Assert.IsType<ListResponseModel<SendResponseModel>>(result);
Assert.Empty(result.Data);
await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());
}
}

View File

@@ -0,0 +1,169 @@
using System.Security.Claims;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures.Queries;
using NSubstitute;
using Xunit;
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();
private readonly ClaimsPrincipal _user;
public SendOwnerQueryTests()
{
_sendRepository = Substitute.For<ISendRepository>();
_featureService = Substitute.For<IFeatureService>();
_userService = Substitute.For<IUserService>();
_user = new ClaimsPrincipal();
_userService.GetProperUserId(_user).Returns(_currentUserId);
_sendOwnerQuery = new SendOwnerQuery(_sendRepository, _featureService, _userService);
}
[Fact]
public async Task Get_WithValidSendOwnedByUser_ReturnsExpectedSend()
{
// Arrange
var sendId = Guid.NewGuid();
var expectedSend = CreateSend(sendId, _currentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(expectedSend);
// Act
var result = await _sendOwnerQuery.Get(sendId, _user);
// Assert
Assert.Same(expectedSend, result);
await _sendRepository.Received(1).GetByIdAsync(sendId);
}
[Fact]
public async Task Get_WithNonExistentSend_ThrowsNotFoundException()
{
// Arrange
var sendId = Guid.NewGuid();
_sendRepository.GetByIdAsync(sendId).Returns((Send?)null);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
}
[Fact]
public async Task Get_WithSendOwnedByDifferentUser_ThrowsNotFoundException()
{
// Arrange
var sendId = Guid.NewGuid();
var differentUserId = Guid.NewGuid();
var send = CreateSend(sendId, differentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(send);
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(() => _sendOwnerQuery.Get(sendId, _user));
}
[Fact]
public async Task Get_WithNullCurrentUserId_ThrowsBadRequestException()
{
// Arrange
var sendId = Guid.NewGuid();
var send = CreateSend(sendId, _currentUserId);
_sendRepository.GetByIdAsync(sendId).Returns(send);
var nullUser = new ClaimsPrincipal();
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.Get(sendId, nullUser));
Assert.Equal("invalid user.", exception.Message);
}
[Fact]
public async Task GetOwned_WithFeatureFlagEnabled_ReturnsAllSends()
{
// Arrange
var sends = new List<Send>
{
CreateSend(Guid.NewGuid(), _currentUserId, emails: null),
CreateSend(Guid.NewGuid(), _currentUserId, emails: "test@example.com"),
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);
// Assert
Assert.Equal(3, result.Count);
Assert.Contains(sends[0], result);
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<Send> { 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]
public async Task GetOwned_WithNullCurrentUserId_ThrowsBadRequestException()
{
// Arrange
var nullUser = new ClaimsPrincipal();
_userService.GetProperUserId(nullUser).Returns((Guid?)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sendOwnerQuery.GetOwned(nullUser));
Assert.Equal("invalid user.", exception.Message);
}
[Fact]
public async Task GetOwned_WithEmptyCollection_ReturnsEmptyCollection()
{
// Arrange
var emptySends = new List<Send>();
_sendRepository.GetManyByUserIdAsync(_currentUserId).Returns(emptySends);
_featureService.IsEnabled(FeatureFlagKeys.PM19051_ListEmailOtpSends).Returns(true);
// Act
var result = await _sendOwnerQuery.GetOwned(_user);
// Assert
Assert.Empty(result);
await _sendRepository.Received(1).GetManyByUserIdAsync(_currentUserId);
}
private static Send CreateSend(Guid id, Guid userId, string? emails = null)
{
return new Send
{
Id = id,
UserId = userId,
Emails = emails
};
}
}