mirror of
https://github.com/bitwarden/server.git
synced 2026-06-01 12:26:46 -05:00
* 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.
145 lines
5.6 KiB
C#
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);
|
|
}
|
|
}
|