Files
server/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationInviteLinksControllerTests.cs
Rui Tomé 9e4bef5ee7 [PM-34776/PM-37797] Add invite link email domain validation endpoint (#7683)
* Add email domain validation to OrganizationInviteLink

- Introduced IsEmailDomainAllowed method to check if an email's domain is permitted based on allowed domains.
- Added necessary using directives for MailAddress and domain sanitization utilities.

* Refactor OrganizationInviteLink by removing email domain validation

- Removed the IsEmailDomainAllowed method and associated using directives for MailAddress.
- Cleaned up the code by eliminating unused methods related to email domain validation.

* Add InviteLinkDomainValidator for email domain validation

- Introduced InviteLinkDomainValidator class with IsEmailDomainAllowed method to validate if an email's domain is in the list of allowed domains.
- Utilized MailAddress for email parsing and added domain sanitization logic.

* Add email domain validation endpoint for organization invite links

- Implemented ValidateEmailDomain method in OrganizationInviteLinksController to check if an email's domain is allowed based on the invite link's permitted domains.
- Created OrganizationInviteLinkValidateEmailDomainRequestModel for request validation and OrganizationInviteLinkValidateEmailDomainResponseModel for response formatting.
- Integrated IOrganizationInviteLinkRepository to retrieve invite link details by code.

* Add unit tests for InviteLinkDomainValidator

- Created InviteLinkDomainValidatorTests class to validate email domain functionality.
- Added tests for various scenarios including invalid emails, empty domain lists, and matching domains.
- Ensured comprehensive coverage of the IsEmailDomainAllowed method's behavior.

* Add integration test for email domain validation in OrganizationInviteLinksController

- Implemented a test to validate that an allowed email domain returns the expected result when checked against an organization invite link.
- Ensured the test verifies the creation of an invite link and the subsequent validation of an email domain against the allowed domains list.

* Add validation query and interface for organization invite link email domain

- Introduced ValidateOrganizationInviteLinkEmailDomainQuery class to validate if an email's domain is allowed based on the invite link's permitted domains.
- Created IValidateOrganizationInviteLinkEmailDomainQuery interface to define the validation method.
- Added unit tests for the validation query to ensure correct behavior for various scenarios, including link not found and domain matching.

* Refactor OrganizationInviteLinksController to use validation query for email domain

- Updated ValidateEmailDomain method to utilize IValidateOrganizationInviteLinkEmailDomainQuery for domain validation instead of directly accessing the repository.
- Removed unnecessary repository dependency and streamlined the response handling for validation results.
- Registered the new validation query in OrganizationServiceCollectionExtensions for dependency injection.

* Refactor InviteLinkDomainValidator and replace MailAddress usage with existing email validation method
2026-05-29 12:23:15 +01:00

267 lines
11 KiB
C#

using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationInviteLinksControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private const string _validEncryptedKey =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private Organization _organization = null!;
private string _ownerEmail = null!;
public OrganizationInviteLinksControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.GenerateInviteLink)
.Returns(true);
});
_factory.SubstituteService<IApplicationCacheService>(cacheService =>
{
cacheService
.GetOrganizationAbilityAsync(Arg.Any<Guid>())
.Returns(new OrganizationAbility { UseInviteLinks = true });
});
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"integration-test{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(
_factory,
plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail,
passwordManagerSeats: 10,
paymentMethod: PaymentMethodType.Card);
await _loginHelper.LoginAsync(_ownerEmail);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task ValidateEmailDomain_WithAllowedEmail_ReturnsIsAllowedTrue()
{
var createRequest = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = _validEncryptedKey,
};
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", createRequest);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(created);
var validateRequest = new OrganizationInviteLinkValidateEmailDomainRequestModel
{
Code = created.Code,
Email = "user@acme.com",
};
using var anonymousClient = _factory.CreateClient();
var validateResponse = await anonymousClient.PostAsJsonAsync(
"/organizations/invite-link/validate-email-domain", validateRequest);
Assert.Equal(HttpStatusCode.OK, validateResponse.StatusCode);
var result = await validateResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkValidateEmailDomainResponseModel>();
Assert.NotNull(result);
Assert.True(result.IsAllowed);
}
[Fact]
public async Task CreateThenGet_AsOwner_ReturnsCreatedAndOk()
{
var request = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com", "example.com"],
EncryptedInviteKey = _validEncryptedKey,
};
static void AssertInviteLink(OrganizationInviteLinkResponseModel? content, Organization organization)
{
Assert.NotNull(content);
Assert.NotEqual(Guid.Empty, content.Id);
Assert.NotEqual(Guid.Empty, content.Code);
Assert.Equal(organization.Id, content.OrganizationId);
Assert.Equal(["acme.com", "example.com"], content.AllowedDomains);
Assert.Equal(_validEncryptedKey, content.EncryptedInviteKey);
}
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", request);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
AssertInviteLink(created, _organization);
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var content = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
AssertInviteLink(content, _organization);
}
[Fact]
public async Task CreateThenUpdateThenGet_AsOwner_ReturnsCreatedAndOkAndOk()
{
var createRequest = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = _validEncryptedKey,
};
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", createRequest);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(created);
var updateRequest = new UpdateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["example.com", "new.com"],
};
var updateResponse = await _client.PutAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", updateRequest);
Assert.Equal(HttpStatusCode.OK, updateResponse.StatusCode);
var updated = await updateResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(updated);
Assert.Equal(created.Id, updated.Id);
Assert.Equal(created.Code, updated.Code);
Assert.Equal(_organization.Id, updated.OrganizationId);
Assert.Equal(_validEncryptedKey, updated.EncryptedInviteKey);
Assert.Equal(["example.com", "new.com"], updated.AllowedDomains);
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var content = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(content);
Assert.Equal(created.Id, content.Id);
Assert.Equal(created.Code, content.Code);
Assert.Equal(_validEncryptedKey, content.EncryptedInviteKey);
Assert.Equal(["example.com", "new.com"], content.AllowedDomains);
}
[Fact]
public async Task Delete_AsOwner_ReturnsNoContentAndRemovesLink()
{
var createRequest = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = _validEncryptedKey,
};
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", createRequest);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var deleteResponse = await _client.DeleteAsync(
$"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
var deleteAgainResponse = await _client.DeleteAsync(
$"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.NotFound, deleteAgainResponse.StatusCode);
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}
[Fact]
public async Task Refresh_AsOwner_ReplacesLink()
{
var createRequest = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com", "example.com"],
EncryptedInviteKey = _validEncryptedKey,
};
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", createRequest);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var original = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(original);
var refreshRequest = new RefreshOrganizationInviteLinkRequestModel
{
EncryptedInviteKey = _validEncryptedKey,
};
var refreshResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link/refresh", refreshRequest);
Assert.Equal(HttpStatusCode.OK, refreshResponse.StatusCode);
var refreshed = await refreshResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(refreshed);
Assert.NotEqual(original.Id, refreshed.Id);
Assert.NotEqual(original.Code, refreshed.Code);
Assert.Equal(original.AllowedDomains, refreshed.AllowedDomains);
Assert.Equal(_organization.Id, refreshed.OrganizationId);
var getResponse = await _client.GetAsync($"/organizations/{_organization.Id}/invite-link");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var current = await getResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(current);
Assert.Equal(refreshed.Id, current.Id);
}
[Fact]
public async Task GetStatus_WithExistingLink_ReturnsData()
{
var createRequest = new CreateOrganizationInviteLinkRequestModel
{
AllowedDomains = ["acme.com"],
EncryptedInviteKey = _validEncryptedKey,
};
var createResponse = await _client.PostAsJsonAsync(
$"/organizations/{_organization.Id}/invite-link", createRequest);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkResponseModel>();
Assert.NotNull(created);
var anonClient = _factory.CreateClient();
var statusResponse = await anonClient.PostAsJsonAsync(
"/organizations/invite-link/status",
new GetOrganizationInviteLinkStatusRequestModel { Code = created.Code });
Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);
var status = await statusResponse.Content.ReadFromJsonAsync<OrganizationInviteLinkStatusResponseModel>();
Assert.NotNull(status);
Assert.Equal(_organization.Name, status.OrganizationName);
Assert.True(status.SeatsAvailable);
}
}