1
0
mirror of https://github.com/bitwarden/server synced 2026-01-01 16:13:33 +00:00

[PM-27766] Add policy for blocking account creation from claimed domains. (#6537)

* Add policy for blocking account creation from claimed domains.

* dotnet format

* check as part of email verification

* add feature flag

* fix tests

* try to fix dates on database integration tests

* PR feedback from claude

* remove claude local settings

* pr feedback

* format

* fix test

* create or alter

* PR feedback

* PR feedback

* Update src/Core/Constants.cs

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>

* fix merge issues

* fix tests

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Kyle Spearrin
2025-11-19 20:25:50 -05:00
committed by GitHub
parent 55fb80b2fc
commit c0700a6946
18 changed files with 1502 additions and 18 deletions

View File

@@ -0,0 +1,189 @@
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Services;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
[SutProviderCustomize]
public class BlockClaimedDomainAccountCreationPolicyValidatorTests
{
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_NoVerifiedDomains_ValidationError(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Equal("You must claim at least one domain to turn on this policy", result);
}
[Theory, BitAutoData]
public async Task ValidateAsync_EnablingPolicy_HasVerifiedDomains_Success(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_DisablingPolicy_NoValidation(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.DidNotReceive()
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_NoVerifiedDomains_ValidationError(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(false);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
// Assert
Assert.Equal("You must claim at least one domain to turn on this policy", result);
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_EnablingPolicy_HasVerifiedDomains_Success(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)
.Returns(true);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
}
[Theory, BitAutoData]
public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_NoValidation(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, false)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel());
// Act
var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, null);
// Assert
Assert.True(string.IsNullOrEmpty(result));
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.DidNotReceive()
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task ValidateAsync_FeatureFlagDisabled_ReturnsError(
[PolicyUpdate(PolicyType.BlockClaimedDomainAccountCreation, true)] PolicyUpdate policyUpdate,
SutProvider<BlockClaimedDomainAccountCreationPolicyValidator> sutProvider)
{
// Arrange
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(false);
// Act
var result = await sutProvider.Sut.ValidateAsync(policyUpdate, null);
// Assert
Assert.Equal("This feature is not enabled", result);
await sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
.DidNotReceive()
.HasVerifiedDomainsAsync(Arg.Any<Guid>());
}
[Fact]
public void Type_ReturnsBlockClaimedDomainAccountCreation()
{
// Arrange
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
// Act & Assert
Assert.Equal(PolicyType.BlockClaimedDomainAccountCreation, validator.Type);
}
[Fact]
public void RequiredPolicies_ReturnsEmpty()
{
// Arrange
var validator = new BlockClaimedDomainAccountCreationPolicyValidator(null, null);
// Act
var requiredPolicies = validator.RequiredPolicies.ToList();
// Assert
Assert.Empty(requiredPolicies);
}
}

View File

@@ -38,6 +38,12 @@ public class RegisterUserCommandTests
public async Task RegisterUser_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
@@ -62,6 +68,12 @@ public class RegisterUserCommandTests
public async Task RegisterUser_WhenCreateUserFails_ReturnsIdentityResultFailed(SutProvider<RegisterUserCommand> sutProvider, User user)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Failed());
@@ -416,6 +428,138 @@ public class RegisterUserCommandTests
Assert.Equal(expectedErrorMessage, exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
{
// Arrange
user.Email = "user@blocked-domain.com";
orgUser.Email = user.Email;
orgUser.Id = orgUserId;
var blockingOrganizationId = Guid.NewGuid(); // Different org that has the domain blocked
orgUser.OrganizationId = Guid.NewGuid(); // The org they're trying to join
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = orgInviteTokenable;
return true;
});
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId)
.Returns(orgUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Mock the new overload that excludes the organization - it should return true (domain IS blocked by another org)
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
{
// Arrange
user.Email = "user@company-domain.com";
user.ReferenceData = null;
orgUser.Email = user.Email;
orgUser.Id = orgUserId;
// The organization owns the domain and is trying to invite the user
orgUser.OrganizationId = Guid.NewGuid();
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = orgInviteTokenable;
return true;
});
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId)
.Returns(orgUser);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Mock the new overload - it should return false (domain is NOT blocked by OTHER orgs)
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId)
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
// Act
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
// Assert
Assert.True(result.Succeeded);
await sutProvider.GetDependency<IOrganizationDomainRepository>()
.Received(1)
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("company-domain.com", orgUser.OrganizationId);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationInviteToken_WithValidTokenButNullOrgUser_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
{
// Arrange
user.Email = "user@example.com";
orgUser.Email = user.Email;
orgUser.Id = orgUserId;
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = orgInviteTokenable;
return true;
});
// Mock GetByIdAsync to return null - simulating a deleted or non-existent organization user
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(orgUserId)
.Returns((OrganizationUser)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
Assert.Equal("Invalid organization user invitation.", exception.Message);
// Verify that GetByIdAsync was called
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetByIdAsync(orgUserId);
// Verify that user creation was never attempted
await sutProvider.GetDependency<IUserService>()
.DidNotReceive()
.CreateUserAsync(Arg.Any<User>(), Arg.Any<string>());
}
// -----------------------------------------------------------------------------------------------
// RegisterUserViaEmailVerificationToken tests
// -----------------------------------------------------------------------------------------------
@@ -425,6 +569,12 @@ public class RegisterUserCommandTests
public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(callInfo =>
@@ -457,6 +607,12 @@ public class RegisterUserCommandTests
public async Task RegisterUserViaEmailVerificationToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(callInfo =>
@@ -495,6 +651,12 @@ public class RegisterUserCommandTests
string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
.Returns((true, new OrganizationSponsorship()));
@@ -524,6 +686,12 @@ public class RegisterUserCommandTests
string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
.Returns((false, new OrganizationSponsorship()));
@@ -561,9 +729,14 @@ public class RegisterUserCommandTests
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
emergencyAccess.Email = user.Email;
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
@@ -597,9 +770,14 @@ public class RegisterUserCommandTests
string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
emergencyAccess.Email = "wrong@email.com";
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
@@ -640,6 +818,8 @@ public class RegisterUserCommandTests
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
@@ -662,6 +842,10 @@ public class RegisterUserCommandTests
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
@@ -691,6 +875,8 @@ public class RegisterUserCommandTests
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
@@ -713,6 +899,10 @@ public class RegisterUserCommandTests
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
// Using sutProvider in the parameters of the function means that the constructor has already run for the
// command so we have to recreate it in order for our mock overrides to be used.
sutProvider.Create();
@@ -762,6 +952,66 @@ public class RegisterUserCommandTests
}
// -----------------------------------------------------------------------------------------------
// Domain blocking tests (BlockClaimedDomainAccountCreation policy)
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task RegisterUser_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user)
{
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUser(user));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
// Verify user creation was never attempted
await sutProvider.GetDependency<IUserService>()
.DidNotReceive()
.CreateUserAsync(Arg.Any<User>());
}
[Theory]
[BitAutoData]
public async Task RegisterUser_AllowedDomain_Succeeds(
SutProvider<RegisterUserCommand> sutProvider, User user)
{
// Arrange
user.Email = "user@allowed-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com")
.Returns(false);
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user)
.Returns(IdentityResult.Success);
// Act
var result = await sutProvider.Sut.RegisterUser(user);
// Assert
Assert.True(result.Succeeded);
await sutProvider.GetDependency<IOrganizationDomainRepository>()
.Received(1)
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowed-domain.com");
}
// SendWelcomeEmail tests
// -----------------------------------------------------------------------------------------------
[Theory]
@@ -799,6 +1049,194 @@ public class RegisterUserCommandTests
.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.Name);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaEmailVerificationToken_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
string emailVerificationToken, bool receiveMarketingMaterials)
{
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(callInfo =>
{
callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);
return true;
});
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
user.Email = "user@blocked-domain.com";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
.Returns((true, new OrganizationSponsorship()));
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = "user@blocked-domain.com";
emergencyAccess.Email = user.Email;
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);
return true;
});
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaProviderInviteToken_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
user.Email = "user@blocked-domain.com";
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
// Get the byte array of the plaintext
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
// Base64 encode the byte array (this is passed to protector.protect(bytes))
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
var mockDataProtector = Substitute.For<IDataProtector>();
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(mockDataProtector);
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com")
.Returns(true);
// Using sutProvider in the parameters of the function means that the constructor has already run for the
// command so we have to recreate it in order for our mock overrides to be used.
sutProvider.Create();
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
// -----------------------------------------------------------------------------------------------
// Invalid email format tests
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task RegisterUser_InvalidEmailFormat_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user)
{
// Arrange
user.Email = "invalid-email-format";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUser(user));
Assert.Equal("Invalid email address format.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaEmailVerificationToken_InvalidEmailFormat_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
string emailVerificationToken, bool receiveMarketingMaterials)
{
// Arrange
user.Email = "invalid-email-format";
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(callInfo =>
{
callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);
return true;
});
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));
Assert.Equal("Invalid email address format.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SendWelcomeEmail_OrganizationNull_SendsIndividualWelcomeEmail(

View File

@@ -21,9 +21,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationTrue_SendsEmailAndReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.ReturnsNull();
@@ -34,6 +36,10 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
sutProvider.GetDependency<IMailService>()
.SendRegistrationVerificationEmailAsync(email, Arg.Any<string>())
.Returns(Task.CompletedTask);
@@ -56,9 +62,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationTrue_ReturnsNull(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(new User());
@@ -69,6 +77,10 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
var mockedToken = "token";
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
@@ -87,9 +99,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenIsNewUserAndEnableEmailVerificationFalse_ReturnsToken(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.ReturnsNull();
@@ -100,6 +114,10 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
var mockedToken = "token";
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
@@ -128,9 +146,11 @@ public class SendVerificationEmailForRegistrationCommandTests
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenIsExistingUserAndEnableEmailVerificationFalse_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string email, string name, bool receiveMarketingEmails)
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@example.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(new User());
@@ -138,6 +158,13 @@ public class SendVerificationEmailForRegistrationCommandTests
sutProvider.GetDependency<GlobalSettings>()
.EnableEmailVerification = false;
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(Arg.Any<string>())
.Returns(false);
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
}
@@ -162,4 +189,88 @@ public class SendVerificationEmailForRegistrationCommandTests
.DisableUserRegistration = false;
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.Run("", name, receiveMarketingEmails));
}
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenBlockedDomain_ThrowsBadRequestException(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@blockedcompany.com";
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blockedcompany.com")
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.Run(email, name, receiveMarketingEmails));
Assert.Equal("This email address is claimed by an organization using Bitwarden.", exception.Message);
}
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_WhenAllowedDomain_Succeeds(SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string name, bool receiveMarketingEmails)
{
// Arrange
var email = $"test+{Guid.NewGuid()}@allowedcompany.com";
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.ReturnsNull();
sutProvider.GetDependency<GlobalSettings>()
.EnableEmailVerification = false;
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
sutProvider.GetDependency<IOrganizationDomainRepository>()
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("allowedcompany.com")
.Returns(false);
var mockedToken = "token";
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
.Protect(Arg.Any<RegistrationEmailVerificationTokenable>())
.Returns(mockedToken);
// Act
var result = await sutProvider.Sut.Run(email, name, receiveMarketingEmails);
// Assert
Assert.Equal(mockedToken, result);
}
[Theory]
[BitAutoData]
public async Task SendVerificationEmailForRegistrationCommand_InvalidEmailFormat_ThrowsBadRequestException(
SutProvider<SendVerificationEmailForRegistrationCommand> sutProvider,
string name, bool receiveMarketingEmails)
{
// Arrange
var email = "invalid-email-format";
sutProvider.GetDependency<GlobalSettings>()
.DisableUserRegistration = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.BlockClaimedDomainAccountCreation)
.Returns(true);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.Run(email, name, receiveMarketingEmails));
Assert.Equal("Invalid email address format.", exception.Message);
}
}

View File

@@ -0,0 +1,51 @@
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.Utilities;
public class EmailValidationTests
{
[Theory]
[InlineData("user@Example.COM", "example.com")]
[InlineData("user@EXAMPLE.COM", "example.com")]
[InlineData("user@example.com", "example.com")]
[InlineData("user@Example.Com", "example.com")]
[InlineData("User@DOMAIN.CO.UK", "domain.co.uk")]
public void GetDomain_WithMixedCaseEmail_ReturnsLowercaseDomain(string email, string expectedDomain)
{
// Act
var result = EmailValidation.GetDomain(email);
// Assert
Assert.Equal(expectedDomain, result);
}
[Theory]
[InlineData("hello@world.com", "world.com")] // regular email address
[InlineData("hello@world.planet.com", "world.planet.com")] // subdomain
[InlineData("hello+1@world.com", "world.com")] // alias
[InlineData("hello.there@world.com", "world.com")] // period in local-part
[InlineData("hello@wörldé.com", "wörldé.com")] // unicode domain
[InlineData("hello@world.cömé", "world.cömé")] // unicode top-level domain
public void GetDomain_WithValidEmail_ReturnsLowercaseDomain(string email, string expectedDomain)
{
// Act
var result = EmailValidation.GetDomain(email);
// Assert
Assert.Equal(expectedDomain, result);
}
[Theory]
[InlineData("invalid-email")]
[InlineData("@example.com")]
[InlineData("user@")]
[InlineData("")]
public void GetDomain_WithInvalidEmail_ThrowsBadRequestException(string email)
{
// Act & Assert
var exception = Assert.Throws<BadRequestException>(() => EmailValidation.GetDomain(email));
Assert.Equal("Invalid email address format.", exception.Message);
}
}

View File

@@ -242,7 +242,7 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
var orgInviteToken = "BwOrgUserInviteToken_CfDJ8HOzu6wr6nVLouuDxgOHsMwPcj9Guuip5k_XLD1bBGpwQS1f66c9kB6X4rvKGxNdywhgimzgvG9SgLwwJU70O8P879XyP94W6kSoT4N25a73kgW3nU3vl3fAtGSS52xdBjNU8o4sxmomRvhOZIQ0jwtVjdMC2IdybTbxwCZhvN0hKIFs265k6wFRSym1eu4NjjZ8pmnMneG0PlKnNZL93tDe8FMcqStJXoddIEgbA99VJp8z1LQmOMfEdoMEM7Zs8W5bZ34N4YEGu8XCrVau59kGtWQk7N4rPV5okzQbTpeoY_4FeywgLFGm-tDtTPEdSEBJkRjexANri7CGdg3dpnMifQc_bTmjZd32gOjw8N8v";
var orgUserId = new Guid("5e45fbdc-a080-4a77-93ff-b19c0161e81e");
var orgUser = new OrganizationUser { Id = orgUserId, Email = email };
var orgUser = new OrganizationUser { Id = orgUserId, Email = email, OrganizationId = Guid.NewGuid() };
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser)
{
@@ -259,6 +259,12 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
});
});
localFactory.SubstituteService<IOrganizationUserRepository>(orgUserRepository =>
{
orgUserRepository.GetByIdAsync(orgUserId)
.Returns(orgUser);
});
var registerFinishReqModel = new RegisterFinishRequestModel
{
Email = email,

View File

@@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Xunit;
@@ -7,7 +9,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
public class OrganizationDomainRepositoryTests
{
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldReturn3DaysOldUnverifiedDomains(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -74,7 +76,7 @@ public class OrganizationDomainRepositoryTests
Assert.NotNull(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnDomainsUnder3DaysOld(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -120,7 +122,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetExpiredOrganizationDomainsAsync_ShouldNotReturnVerifiedDomains(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
@@ -189,7 +191,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain2);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldReturnUnverifiedDomains(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -228,7 +230,7 @@ public class OrganizationDomainRepositoryTests
Assert.NotNull(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldNotReturnUnverifiedDomains_WhenNextRunDateIsOutside36hoursWindow(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -267,7 +269,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetManyByNextRunDateAsync_ShouldNotReturnVerifiedDomains(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -307,7 +309,7 @@ public class OrganizationDomainRepositoryTests
Assert.Null(expectedDomain);
}
[DatabaseTheory, DatabaseData]
[Theory, DatabaseData]
public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
@@ -383,4 +385,437 @@ public class OrganizationDomainRepositoryTests
Assert.Null(otherOrganizationDomain);
Assert.Null(unverifiedDomain);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithVerifiedDomainAndBlockPolicy_ReturnsTrue(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.True(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUnverifiedDomain_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
// Do not verify the domain
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledPolicy_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = false
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithDisabledOrganization_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = false,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUsePoliciesFalse_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = false, // Organization doesn't have policies feature
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithUseOrganizationDomainsFalse_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = false // Organization doesn't have organization domains feature
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNoPolicyOfType_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
// No policy created
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_WithNonExistentDomain_ReturnsFalse(
IOrganizationDomainRepository organizationDomainRepository)
{
// Arrange
var domainName = $"nonexistent-{Guid.NewGuid()}.example.com";
// Act
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName);
// Assert
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenSameOrg_ReturnsFalse(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = $"test+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain.SetNextRunDate(1);
organizationDomain.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain);
var policy = new Policy
{
OrganizationId = organization.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy);
// Act - Exclude the same organization that has the domain
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization.Id);
// Assert - Should return false because we're excluding the only org with this domain
Assert.False(result);
}
[Theory, DatabaseData]
public async Task HasVerifiedDomainWithBlockClaimedDomainPolicyAsync_ExcludeOrganization_WhenDifferentOrg_ReturnsTrue(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository,
IPolicyRepository policyRepository)
{
// Arrange
var id = Guid.NewGuid();
var domainName = $"test-{id}.example.com";
var organization1 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 1 {id}",
BillingEmail = $"test1+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
var organizationDomain1 = new OrganizationDomain
{
OrganizationId = organization1.Id,
DomainName = domainName,
Txt = "btw+12345"
};
organizationDomain1.SetNextRunDate(1);
organizationDomain1.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain1);
var policy1 = new Policy
{
OrganizationId = organization1.Id,
Type = PolicyType.BlockClaimedDomainAccountCreation,
Enabled = true
};
await policyRepository.CreateAsync(policy1);
// Create a second organization (the one we'll exclude)
var organization2 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 2 {id}",
BillingEmail = $"test2+{id}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
Enabled = true,
UsePolicies = true,
UseOrganizationDomains = true
});
// Act - Exclude organization2 (but organization1 still has the domain blocked)
var result = await organizationDomainRepository.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync(domainName, organization2.Id);
// Assert - Should return true because organization1 (not excluded) has the domain blocked
Assert.True(result);
}
}