Files
server/test/Api.Test/AdminConsole/Attributes/BindOrganizationAttributeTests.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

123 lines
4.2 KiB
C#

using Bit.Api.AdminConsole.Attributes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Attributes;
public class BindOrganizationAttributeTests
{
private readonly IOrganizationRepository _organizationRepository;
private readonly Organization _organization;
private readonly Guid _orgId;
public BindOrganizationAttributeTests()
{
_organizationRepository = Substitute.For<IOrganizationRepository>();
_orgId = Guid.NewGuid();
_organization = new Organization { Id = _orgId };
}
[Fact]
public async Task BindModelAsync_OrganizationExists_BindsSuccessfully()
{
var binder = new OrganizationModelBinder();
_organizationRepository.GetByIdAsync(_orgId).Returns(_organization);
var context = CreateBindingContext();
await binder.BindModelAsync(context);
Assert.True(context.Result.IsModelSet);
Assert.Equal(_organization, context.Result.Model);
}
[Fact]
public async Task BindModelAsync_OrganizationNotFound_ThrowsNotFoundException()
{
var binder = new OrganizationModelBinder();
_organizationRepository.GetByIdAsync(_orgId).Returns((Organization)null);
var context = CreateBindingContext();
await Assert.ThrowsAsync<NotFoundException>(() => binder.BindModelAsync(context));
}
[Fact]
public async Task BindModelAsync_InvalidOrgId_ThrowsBadRequestException()
{
var binder = new OrganizationModelBinder();
var context = CreateBindingContext(orgIdRouteValue: "not-a-guid");
var exception = await Assert.ThrowsAsync<BadRequestException>(() => binder.BindModelAsync(context));
Assert.Equal("Route parameter 'orgId' or 'organizationId' is missing or invalid.", exception.Message);
}
[Fact]
public async Task BindModelAsync_MissingOrgId_ThrowsBadRequestException()
{
var binder = new OrganizationModelBinder();
var context = CreateBindingContext(includeOrgId: false);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => binder.BindModelAsync(context));
Assert.Equal("Route parameter 'orgId' or 'organizationId' is missing or invalid.", exception.Message);
}
[Fact]
public async Task BindModelAsync_OrganizationIdRouteParam_ResolvesOrgId()
{
var binder = new OrganizationModelBinder();
_organizationRepository.GetByIdAsync(_orgId).Returns(_organization);
var context = CreateBindingContext(useOrganizationIdRoute: true);
await binder.BindModelAsync(context);
Assert.True(context.Result.IsModelSet);
Assert.Equal(_organization, context.Result.Model);
}
private DefaultModelBindingContext CreateBindingContext(
string orgIdRouteValue = null,
bool includeOrgId = true,
bool useOrganizationIdRoute = false)
{
var httpContext = new DefaultHttpContext();
var services = new ServiceCollection();
services.AddScoped(_ => _organizationRepository);
httpContext.RequestServices = services.BuildServiceProvider();
var routeData = new RouteData();
if (includeOrgId)
{
var key = useOrganizationIdRoute ? "organizationId" : "orgId";
routeData.Values[key] = orgIdRouteValue ?? _orgId.ToString();
}
httpContext.Request.RouteValues = routeData.Values;
var actionContext = new ActionContext(
httpContext,
routeData,
new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor(),
new ModelStateDictionary());
var metadataProvider = new EmptyModelMetadataProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(Organization));
return new DefaultModelBindingContext
{
ActionContext = actionContext,
ModelMetadata = metadata,
ModelName = "organization"
};
}
}