Files
server/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/ConfirmManyOrganizationUsersTests.cs
Jared eacafaecfe [PM-33951] feat(admin-console): Add bulk confirmation and pending auto-confirmation (#7661)
* feat(admin-console): Add bulk confirmation and pending auto-confirmation methods for organization users

- Implemented ConfirmManyOrganizationUsersAsync to confirm multiple users in a single operation.
- Added GetManyPendingAutoConfirmAsync to retrieve users pending automatic confirmation.
- Created stored procedures for bulk confirmation and fetching pending users.
- Updated relevant repository interfaces and implementations across Dapper and Entity Framework.

* refactor(admin-console): Change parameter type for ConfirmManyOrganizationUsersAsync to IReadOnlyCollection

- Updated the ConfirmManyOrganizationUsersAsync method signature in the IOrganizationUserRepository and its implementations to use IReadOnlyCollection instead of IEnumerable for better performance and clarity.
- Adjusted related repository methods in both Dapper and Entity Framework implementations to reflect this change.
- Added unit tests to ensure the new implementation behaves as expected, including scenarios for mixed batches and idempotency.

* Remove OrganizationUser_ReadByOrganizationIdStatus stored procedure as part of database cleanup.

* Add integration tests for ConfirmManyOrganizationUsers and GetManyPendingAutoConfirm methods

- Introduced ConfirmManyOrganizationUsersTests to validate the confirmation of multiple organization users, ensuring only accepted users are confirmed and idempotency is maintained.
- Added GetManyPendingAutoConfirmTests to verify retrieval of pending auto-confirm users, ensuring only eligible users are returned based on specific criteria.
- Removed duplicate test implementations from OrganizationUserRepositoryTests to maintain clarity and organization in the test suite.

* Implement OrganizationUser_UpdateStatusKey stored procedure and update related repository method

- Added OrganizationUser_UpdateStatusKey stored procedure to handle updating the status and key of organization users based on a JSON input.
- Updated OrganizationUserRepository to call the new stored procedure instead of the previous confirmation procedure.
- Modified OrganizationUser_ReadByPendingAutoConfirm stored procedure to filter users by a new type value.
- Enhanced integration tests to verify the correct behavior of the updated confirmation logic, ensuring revision dates are accurately tracked.

* Refactor OrganizationUser_UpdateStatusKey to OrganizationUser_UpdateManyStatusKey

- Renamed the stored procedure to OrganizationUser_UpdateManyStatusKey to better reflect its functionality of updating multiple organization users' statuses.
- Updated the OrganizationUserRepository to call the new stored procedure.
- Adjusted the migration script to create or alter the procedure accordingly.

* Update data type for Key column in AddOrganizationUserUpdateStatusKey migration script

- Changed the data type of the Key column from NVARCHAR(MAX) to VARCHAR(MAX) in the UsersToUpdate table and the corresponding JSON parsing logic to improve compatibility and performance.

* Updated spacing

* Add stored procedures for organization user status updates and retrieval

- Created OrganizationUser_UpdateManyStatusKey to update multiple organization users' statuses based on a JSON input, including handling revision dates and tracking updated IDs for idempotency.
- Added OrganizationUser_ReadByPendingAutoConfirm to retrieve organization users pending auto-confirmation based on organization ID and specific status and type filters.

---------

Co-authored-by: mkincaid-bw <mkincaid@bitwarden.com>
2026-05-27 18:05:57 -04:00

83 lines
3.8 KiB
C#

using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
public class ConfirmManyOrganizationUsersTests
{
[Theory, DatabaseData]
public async Task ConfirmManyOrganizationUsersAsync_MixedBatch_ReturnsOnlyAcceptedIds(
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var org = await organizationRepository.CreateTestOrganizationAsync();
var acceptedUser = await userRepository.CreateTestUserAsync("accepted");
var confirmedUser = await userRepository.CreateTestUserAsync("confirmed");
var acceptedOrgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(org, acceptedUser);
var confirmedOrgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(org, confirmedUser);
var usersToConfirm = new[]
{
new AcceptedOrganizationUserToConfirm { OrganizationUserId = acceptedOrgUser.Id, UserId = acceptedUser.Id, Key = "key-accepted" },
new AcceptedOrganizationUserToConfirm { OrganizationUserId = confirmedOrgUser.Id, UserId = confirmedUser.Id, Key = "key-already-confirmed" },
new AcceptedOrganizationUserToConfirm { OrganizationUserId = Guid.NewGuid(), UserId = Guid.NewGuid(), Key = "key-nonexistent" },
};
// Act
var before = DateTime.UtcNow;
var confirmedIds = await organizationUserRepository.ConfirmManyOrganizationUsersAsync(usersToConfirm);
// Assert — only the Accepted user's ID is returned
Assert.Single(confirmedIds);
Assert.Contains(acceptedOrgUser.Id, confirmedIds);
// The previously-accepted user is now Confirmed
var updated = await organizationUserRepository.GetByIdAsync(acceptedOrgUser.Id);
Assert.NotNull(updated);
Assert.Equal(OrganizationUserStatusType.Confirmed, updated.Status);
Assert.Equal("key-accepted", updated.Key);
Assert.True(updated.RevisionDate >= before);
// The already-confirmed user's status is unchanged
var unchanged = await organizationUserRepository.GetByIdAsync(confirmedOrgUser.Id);
Assert.NotNull(unchanged);
Assert.Equal(OrganizationUserStatusType.Confirmed, unchanged.Status);
Assert.Null(unchanged.Key);
}
[Theory, DatabaseData]
public async Task ConfirmManyOrganizationUsersAsync_Idempotent_SecondCallWithSameBatchReturnsEmpty(
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var org = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(org, user);
var batch = new[]
{
new AcceptedOrganizationUserToConfirm { OrganizationUserId = orgUser.Id, UserId = user.Id, Key = "key" }
};
// Act
var firstResult = await organizationUserRepository.ConfirmManyOrganizationUsersAsync(batch);
var secondResult = await organizationUserRepository.ConfirmManyOrganizationUsersAsync(batch);
// Assert — first call confirms, second call is a no-op (user is no longer Accepted)
Assert.Single(firstResult);
Assert.Contains(orgUser.Id, firstResult);
Assert.Empty(secondResult);
var finalState = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(finalState);
Assert.Equal(OrganizationUserStatusType.Confirmed, finalState.Status);
}
}