Files
server/test/Api.Test/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs
Rui Tomé 7180015ed6 [PM-37251] Add public invite link GET status endpoint (#7656)
* Implement GetOrganizationInviteLinkStatusQuery to retrieve invite link status

- Added GetOrganizationInviteLinkStatusQuery class to handle fetching the status of an organization invite link based on its code.
- Introduced OrganizationInviteLinkStatus and OrganizationInviteLinkSsoStatus records to encapsulate the invite link status and SSO information.
- Created IGetOrganizationInviteLinkStatusQuery interface to define the contract for the query implementation.

* Add unit tests for GetOrganizationInviteLinkStatusQuery

- Introduced comprehensive unit tests for GetOrganizationInviteLinkStatusQuery to validate various scenarios including successful retrieval of invite link status, handling of not found errors, and seat availability checks.
- Utilized Xunit and NSubstitute for testing and mocking dependencies, ensuring robust coverage of the query's functionality.

* Add IGetOrganizationInviteLinkStatusQuery to service collection

- Registered IGetOrganizationInviteLinkStatusQuery with the service collection to enable retrieval of organization invite link status.
- This addition supports the recently implemented GetOrganizationInviteLinkStatusQuery functionality.

* Add OrganizationInviteLinksPublicController and response models

- Introduced OrganizationInviteLinksPublicController to handle requests for organization invite link status.
- Implemented GetStatus endpoint to retrieve the status of an invite link using its GUID code.
- Added OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkSsoResponseModel to structure the response data for the invite link status.
- Ensured the endpoint is accessible to anonymous users while requiring application authorization for other actions.

* Add integration tests for OrganizationInviteLinksPublicController

- Introduced integration tests for OrganizationInviteLinksPublicController to validate the GetStatus endpoint functionality.
- Implemented tests to ensure correct handling of existing invite links and appropriate responses for valid and not found scenarios.
- Utilized Xunit and NSubstitute for testing and mocking dependencies, enhancing test coverage for invite link status retrieval.

* Updated GetOrganizationInviteLinkStatusQuery to return SSO status based on organization settings, including UseSso and UsePolicies

* Move status endpoint into OrganizationInviteLinksController as POST

* Refactor OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkStatus to remove OrganizationId property

- Removed OrganizationId property from both OrganizationInviteLinkStatusResponseModel and OrganizationInviteLinkStatus records to streamline the data model.
- Updated constructors accordingly to reflect the changes in the response models.

* Refactor GetOrganizationInviteLinkStatusQuery to simplify organization checks

- Updated the logic in GetOrganizationInviteLinkStatusQuery to streamline organization validation by combining null and enabled checks.
- Removed the dependency on IApplicationCacheService and adjusted the seat availability logic to enhance clarity and efficiency.
- Modified the return statement to use organization name directly instead of organization ID.

* Add integration tests for OrganizationInviteLinksController

- Introduced a new test method to validate the GetStatus functionality for existing invite links in OrganizationInviteLinksControllerTests.
- Enhanced existing tests to ensure correct responses for valid and not found scenarios.
- Removed OrganizationInviteLinksPublicControllerTests as its functionality is now covered in the OrganizationInviteLinksControllerTests.

* Refactor OrganizationInviteLinksControllerTests

- Updated test methods in OrganizationInviteLinksControllerTests to utilize GetOrganizationInviteLinkStatusRequestModel instead of individual parameters.
- Added a new test case to handle scenarios where the invite link status is not available, returning a BadRequest response.
- Enhanced existing tests to ensure consistent handling of valid and not found scenarios.

* Update GetOrganizationInviteLinkStatusQueryTests to enable organization for invite link tests
2026-05-21 16:27:54 +01:00

266 lines
10 KiB
C#

using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks;
using Bit.Core.AdminConsole.OrganizationFeatures.InviteLinks.Interfaces;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using NSubstitute;
using Xunit;
using ErrorResponseModel = Bit.Core.Models.Api.ErrorResponseModel;
namespace Bit.Api.Test.AdminConsole.Controllers;
[ControllerCustomize(typeof(OrganizationInviteLinksController))]
[SutProviderCustomize]
public class OrganizationInviteLinksControllerTests
{
[Theory, BitAutoData]
public async Task Create_WithValidInput_Success(
Guid orgId,
OrganizationInviteLink inviteLink,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
inviteLink.OrganizationId = orgId;
inviteLink.AllowedDomains = "[\"acme.com\"]";
var model = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = "encrypted-key",
};
sutProvider.GetDependency<ICreateOrganizationInviteLinkCommand>()
.CreateAsync(Arg.Any<CreateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(inviteLink));
var result = await sutProvider.Sut.Create(orgId, model);
var createdResult = Assert.IsType<Created<OrganizationInviteLinkResponseModel>>(result);
Assert.Equal($"organizations/{orgId}/invite-link", createdResult.Location);
Assert.NotNull(createdResult.Value);
Assert.Equal(inviteLink.Id, createdResult.Value.Id);
Assert.Equal(inviteLink.Code, createdResult.Value.Code);
Assert.Equal(orgId, createdResult.Value.OrganizationId);
await sutProvider.GetDependency<ICreateOrganizationInviteLinkCommand>()
.Received(1)
.CreateAsync(Arg.Is<CreateOrganizationInviteLinkRequest>(r =>
r.OrganizationId == orgId &&
r.EncryptedInviteKey == "encrypted-key"));
}
[Theory, BitAutoData]
public async Task Create_WithExistingLink_Returns409(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
var model = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = "encrypted-key",
};
sutProvider.GetDependency<ICreateOrganizationInviteLinkCommand>()
.CreateAsync(Arg.Any<CreateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkAlreadyExists()));
var result = await sutProvider.Sut.Create(orgId, model);
var jsonResult = Assert.IsType<JsonHttpResult<Bit.Core.Models.Api.ErrorResponseModel>>(result);
Assert.Equal(StatusCodes.Status409Conflict, jsonResult.StatusCode);
}
[Theory, BitAutoData]
public async Task Get_WhenLinkExists_ReturnsOkWithModel(
Guid orgId,
OrganizationInviteLink inviteLink,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
inviteLink.OrganizationId = orgId;
inviteLink.AllowedDomains = "[\"acme.com\"]";
sutProvider.GetDependency<IGetOrganizationInviteLinkQuery>()
.GetAsync(orgId)
.Returns(new CommandResult<OrganizationInviteLink>(inviteLink));
var result = await sutProvider.Sut.Get(orgId);
var okResult = Assert.IsType<Ok<OrganizationInviteLinkResponseModel>>(result);
Assert.NotNull(okResult.Value);
Assert.Equal(inviteLink.Id, okResult.Value.Id);
Assert.Equal(orgId, okResult.Value.OrganizationId);
}
[Theory, BitAutoData]
public async Task Get_WhenNoLinkExists_ReturnsNotFound(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkQuery>()
.GetAsync(orgId)
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkNotFound()));
var result = await sutProvider.Sut.Get(orgId);
var notFoundResult = Assert.IsType<NotFound<Bit.Core.Models.Api.ErrorResponseModel>>(result);
Assert.NotNull(notFoundResult.Value);
}
[Theory, BitAutoData]
public async Task Get_WhenInviteLinkNotAvailable_Returns400(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkQuery>()
.GetAsync(orgId)
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkNotAvailable()));
var result = await sutProvider.Sut.Get(orgId);
var badRequestResult = Assert.IsType<BadRequest<Bit.Core.Models.Api.ErrorResponseModel>>(result);
Assert.NotNull(badRequestResult.Value);
}
[Theory, BitAutoData]
public async Task Create_WithValidationError_Returns400(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
var model = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = [],
EncryptedInviteKey = "encrypted-key",
};
sutProvider.GetDependency<ICreateOrganizationInviteLinkCommand>()
.CreateAsync(Arg.Any<CreateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkDomainsRequired()));
var result = await sutProvider.Sut.Create(orgId, model);
var badRequestResult = Assert.IsType<BadRequest<Bit.Core.Models.Api.ErrorResponseModel>>(result);
Assert.NotNull(badRequestResult.Value);
}
[Theory, BitAutoData]
public async Task Update_WithValidInput_ReturnsOk(
Guid orgId,
OrganizationInviteLink inviteLink,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
inviteLink.OrganizationId = orgId;
inviteLink.AllowedDomains = "[\"acme.com\"]";
var model = new UpdateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
};
sutProvider.GetDependency<IUpdateOrganizationInviteLinkCommand>()
.UpdateAsync(Arg.Any<UpdateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(inviteLink));
var result = await sutProvider.Sut.Update(orgId, model);
var okResult = Assert.IsType<Ok<OrganizationInviteLinkResponseModel>>(result);
Assert.NotNull(okResult.Value);
Assert.Equal(inviteLink.Id, okResult.Value.Id);
Assert.Equal(orgId, okResult.Value.OrganizationId);
await sutProvider.GetDependency<IUpdateOrganizationInviteLinkCommand>()
.Received(1)
.UpdateAsync(Arg.Is<UpdateOrganizationInviteLinkRequest>(r =>
r.OrganizationId == orgId));
}
[Theory, BitAutoData]
public async Task Update_WhenNoLinkExists_ReturnsNotFound(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
var model = new UpdateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
};
sutProvider.GetDependency<IUpdateOrganizationInviteLinkCommand>()
.UpdateAsync(Arg.Any<UpdateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkNotFound()));
var result = await sutProvider.Sut.Update(orgId, model);
var notFoundResult = Assert.IsType<NotFound<Bit.Core.Models.Api.ErrorResponseModel>>(result);
Assert.NotNull(notFoundResult.Value);
}
[Theory, BitAutoData]
public async Task Update_WithValidationError_Returns400(
Guid orgId,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
var model = new UpdateOrganizationInviteLinkRequestModel
{
AllowedDomains = [],
};
sutProvider.GetDependency<IUpdateOrganizationInviteLinkCommand>()
.UpdateAsync(Arg.Any<UpdateOrganizationInviteLinkRequest>())
.Returns(new CommandResult<OrganizationInviteLink>(new InviteLinkDomainsRequired()));
var result = await sutProvider.Sut.Update(orgId, model);
var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);
Assert.NotNull(badRequestResult.Value);
}
[Theory, BitAutoData]
public async Task GetStatus_WithValidQuery_Success(
GetOrganizationInviteLinkStatusRequestModel model,
OrganizationInviteLinkStatus status,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkStatusQuery>()
.GetStatusAsync(model.Code)
.Returns(new CommandResult<OrganizationInviteLinkStatus>(status));
var result = await sutProvider.Sut.GetStatus(model);
var okResult = Assert.IsType<Ok<OrganizationInviteLinkStatusResponseModel>>(result);
Assert.Equal(status.OrganizationName, okResult.Value!.OrganizationName);
Assert.Equal(status.SeatsAvailable, okResult.Value.SeatsAvailable);
}
[Theory, BitAutoData]
public async Task GetStatus_WithNotFoundError_ReturnsNotFound(
GetOrganizationInviteLinkStatusRequestModel model,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkStatusQuery>()
.GetStatusAsync(model.Code)
.Returns(new CommandResult<OrganizationInviteLinkStatus>(new InviteLinkNotFound()));
var result = await sutProvider.Sut.GetStatus(model);
Assert.IsType<NotFound<ErrorResponseModel>>(result);
}
[Theory, BitAutoData]
public async Task GetStatus_WithNotAvailableError_ReturnsBadRequest(
GetOrganizationInviteLinkStatusRequestModel model,
SutProvider<OrganizationInviteLinksController> sutProvider)
{
sutProvider.GetDependency<IGetOrganizationInviteLinkStatusQuery>()
.GetStatusAsync(model.Code)
.Returns(new CommandResult<OrganizationInviteLinkStatus>(new InviteLinkNotAvailable()));
var result = await sutProvider.Sut.GetStatus(model);
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
}
}