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; /// /// Integration tests for , /// exercised through the GET reset-password-details endpoint which binds an Organization from the /// orgId route parameter. /// public class OrganizationUsersControllerBindOrganizationTests : IClassFixture, 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(); 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(); 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(); 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(); 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); } }