Files
server/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerBindOrganizationTests.cs
Jared 970cacdc29 [PM-38273] feat(admin-console): Add InjectOrganizationAttribute and OrganizationModelBinder (#7659)
* feat(admin-console): Add InjectOrganizationAttribute and OrganizationModelBinder for automatic organization parameter binding

* feat(admin-console): Introduce BindOrganizationAttribute and OrganizationModelBinder for organization parameter binding with unit tests

* feat(admin-console): Update GetResetPasswordDetails to use BindOrganization for organization parameter

* fix(admin-console): Correct organization ID check in GetResetPasswordDetails method to use bound organization

* Refactor OrganizationUsersControllerTests to use bound organization in GetResetPasswordDetails method

- Updated test cases to pass the organization directly instead of relying on repository calls.
- Ensured that the tests correctly assert NotFoundException when the organization user does not match the bound organization.
- Improved clarity in test setup by explicitly binding the organization to the method calls.

* Fix UTF-8 BOM issue in BindOrganizationAttribute.cs

* Add integration tests for OrganizationUsersController's BindOrganization functionality

- Introduced OrganizationUsersControllerBindOrganizationTests to validate the behavior of the GET reset-password-details endpoint.
- Implemented tests for successful retrieval of reset password details, handling of non-existent organization users, and cases where the user belongs to a different organization.
- Ensured comprehensive coverage of scenarios to verify correct status responses and organization binding logic.
2026-05-28 13:06:25 -04:00

145 lines
5.6 KiB
C#

using System.Net;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
/// <summary>
/// Integration tests for <see cref="Bit.Api.AdminConsole.Attributes.BindOrganizationAttribute"/>,
/// exercised through the GET reset-password-details endpoint which binds an Organization from the
/// <c>orgId</c> route parameter.
/// </summary>
public class OrganizationUsersControllerBindOrganizationTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly ApiApplicationFactory _factory;
private readonly HttpClient _client;
private readonly LoginHelper _loginHelper;
private string _ownerEmail = null!;
public OrganizationUsersControllerBindOrganizationTests(ApiApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"bind-org-test-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task GetResetPasswordDetails_HappyPath_ReturnsOk()
{
// Arrange
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail,
passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var organizationRepository = _factory.GetService<IOrganizationRepository>();
organization.UseResetPassword = true;
await organizationRepository.ReplaceAsync(organization);
await _loginHelper.LoginAsync(_ownerEmail);
var (_, memberOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, organization.Id, OrganizationUserType.User);
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
memberOrgUser.ResetPasswordKey = "encrypted-reset-password-key";
await orgUserRepository.ReplaceAsync(memberOrgUser);
// Act
var response = await _client.GetAsync(
$"organizations/{organization.Id}/users/{memberOrgUser.Id}/reset-password-details");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await organizationRepository.DeleteAsync(organization);
}
[Fact]
public async Task GetResetPasswordDetails_OrgUserNotFound_ReturnsNotFound()
{
// Arrange — org exists and auth passes, but the org user ID in the path does not exist.
// BindOrganizationAttribute successfully binds the org; the endpoint then throws
// NotFoundException because the repository returns null for the unknown org user ID.
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail,
passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var organizationRepository = _factory.GetService<IOrganizationRepository>();
organization.UseResetPassword = true;
await organizationRepository.ReplaceAsync(organization);
await _loginHelper.LoginAsync(_ownerEmail);
// Act — use a random Guid that has no matching OrganizationUser row
var response = await _client.GetAsync(
$"organizations/{organization.Id}/users/{Guid.NewGuid()}/reset-password-details");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
await organizationRepository.DeleteAsync(organization);
}
[Fact]
public async Task GetResetPasswordDetails_OrgUserBelongsToDifferentOrg_ReturnsNotFound()
{
// Arrange — create two separate organizations
var (org1, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail,
passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var secondOwnerEmail = $"bind-org-test-owner2-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(secondOwnerEmail);
var (org2, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
plan: PlanType.EnterpriseAnnually,
ownerEmail: secondOwnerEmail,
passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
var organizationRepository = _factory.GetService<IOrganizationRepository>();
org1.UseResetPassword = true;
await organizationRepository.ReplaceAsync(org1);
// Create a user in org2
var (_, org2MemberOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
_factory, org2.Id, OrganizationUserType.User);
// Log in as owner of org1 (who has ManageAccountRecovery for org1)
await _loginHelper.LoginAsync(_ownerEmail);
// Act — request org1's endpoint but pass an org user ID that belongs to org2
var response = await _client.GetAsync(
$"organizations/{org1.Id}/users/{org2MemberOrgUser.Id}/reset-password-details");
// Assert — the org user's OrganizationId does not match org1, so NotFoundException is thrown
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
await organizationRepository.DeleteAsync(org1);
await organizationRepository.DeleteAsync(org2);
}
}