1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Auth/PM-5092 - Registration with Email verification - Send Email Verification Endpoint (#4173)

* PM-5092 - Add new EnableEmailVerification global setting.

* PM-5092 - WIP - AccountsController.cs - create stub for new     PostRegisterSendEmailVerification

* PM-5092 - RegisterSendEmailVerificationRequestModel

* PM-5092 - Create EmailVerificationTokenable.cs and get started on tests (still WIP).

* PM-5092 - EmailVerificationTokenable.cs finished + tests working.

* PM-5092 - Add token data factory for new EmailVerificationTokenable factory.

* PM-5092 - EmailVerificationTokenable.cs - set expiration to match existing verify email.

* PM-5092 - Get SendVerificationEmailForRegistrationCommand command mostly written + register as scoped.

* PM-5092 - Rename tokenable to be more clear and differentiate it from the existing email verification token.

* PM-5092 - Add new registration verify email method on mail service.

* PM-5092 - Refactor SendVerificationEmailForRegistrationCommand and add call to mail service to send email.

* PM-5092 - NoopMailService.cs needs to implement all interface methods.

* PM-5092 - AccountsController.cs - get PostRegisterSendEmailVerification logic in place.

* PM-5092 - AccountsControllerTests.cs - Add some unit tests - WIP

* PM-5092 - SendVerificationEmailForRegistrationCommandTests

* PM-5092 - Add integration tests for new acct controller method

* PM-5092 - Cleanup unit tests

* PM-5092 - AccountsController.cs - PostRegisterSendEmailVerification - remove modelState invalid check as .NET literally executes this validation pre-method execution.

* PM-5092 - Rename to read better - send verification email > send email verification

* PM-5092 - Revert primary constructor approach so DI works.

* PM-5092 - (1) Cleanup new but now not needed global setting (2) Add custom email for registration verify email.

* PM-5092 - Fix email text

* PM-5092 - (1) Modify ReferenceEvent.cs to allow nullable values for the 2 params which should have been nullable based on the constructor logic (2) Add new ReferenceEventType.cs for email verification register submit (3) Update AccountsController.cs to log new reference event (4) Update tests

* PM-5092 - RegistrationEmailVerificationTokenable - update prefix, purpose, and token id to include registration to differentiate it from the existing email verification token.

* PM-5092 - Per PR feedback, cleanup used dict.

* PM-5092 - formatting pass (manual + dotnet format)

* PM-5092 - Per PR feedback, log reference event after core business logic executes

* PM-5092 - Per PR feedback, add validation + added nullable flag to name as it is optional.

* PM-5092 - Per PR feedback, add constructor validation for required tokenable data

* PM-5092 - RegisterVerifyEmail url now contains email as that is required in client side registration step to create a master key.

* PM-5092 - Add fromEmail flag + some docs

* PM-5092 - ReferenceEvent.cs - Per PR feedback, make SignupInitiationPath and PlanUpgradePath nullable

* PM-5092 - ReferenceEvent.cs - remove nullability per PR feedback

* PM-5092 - Per PR feedback, use default constructor and manually create reference event.

* PM-5092 - Per PR feedback, add more docs!
This commit is contained in:
Jared Snider
2024-06-19 13:54:20 -04:00
committed by GitHub
parent c375c18257
commit b2b1e3de87
23 changed files with 773 additions and 5 deletions

View File

@ -0,0 +1,183 @@
using AutoFixture.Xunit2;
using Bit.Core.Tokens;
namespace Bit.Core.Test.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Business.Tokenables;
using Xunit;
public class RegistrationEmailVerificationTokenableTests
{
// Allow a small tolerance for possible execution delays or clock precision to avoid flaky tests.
private static readonly TimeSpan _timeTolerance = TimeSpan.FromMilliseconds(10);
/// <summary>
/// Tests the default constructor behavior when passed null/default values.
/// </summary>
[Fact]
public void Constructor_NullEmail_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => new RegistrationEmailVerificationTokenable(null, null, default));
}
/// <summary>
/// Tests the default constructor behavior when passed required values but null values for optional props.
/// </summary>
[Theory, AutoData]
public void Constructor_NullOptionalProps_PropertiesSetToDefault(string email)
{
var token = new RegistrationEmailVerificationTokenable(email, null, default);
Assert.Equal(email, token.Email);
Assert.Equal(default, token.Name);
Assert.Equal(default, token.ReceiveMarketingEmails);
}
/// <summary>
/// Tests that when a valid inputs are provided to the constructor, the resulting token properties match the user.
/// </summary>
[Theory, AutoData]
public void Constructor_ValidInputs_PropertiesSetFromInputs(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.Equal(email, token.Email);
Assert.Equal(name, token.Name);
Assert.Equal(receiveMarketingEmails, token.ReceiveMarketingEmails);
}
/// <summary>
/// Tests the default expiration behavior immediately after initialization.
/// </summary>
[Fact]
public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration()
{
var token = new RegistrationEmailVerificationTokenable();
var expectedExpiration = DateTime.UtcNow + SsoEmail2faSessionTokenable.GetTokenLifetime();
Assert.True(expectedExpiration - token.ExpirationDate < _timeTolerance);
}
/// <summary>
/// Tests that a custom expiration date is preserved after token initialization.
/// </summary>
[Fact]
public void Constructor_CustomExpirationDate_ExpirationMatchesProvidedValue()
{
var customExpiration = DateTime.UtcNow.AddHours(3);
var token = new RegistrationEmailVerificationTokenable
{
ExpirationDate = customExpiration
};
Assert.True((customExpiration - token.ExpirationDate).Duration() < _timeTolerance);
}
/// <summary>
/// Tests the validity of a token with a non-matching identifier.
/// </summary>
[Theory, AutoData]
public void Valid_WrongIdentifier_ReturnsFalse(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails) { Identifier = "InvalidIdentifier" };
Assert.False(token.Valid);
}
/// <summary>
/// Tests the token validity when the token is initialized with valid inputs.
/// </summary>
[Theory, AutoData]
public void Valid_ValidInputs_ReturnsTrue(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.True(token.Valid);
}
/// <summary>
/// Tests the token validity when the name is null
/// </summary>
[Theory, AutoData]
public void TokenIsValid_NullName_ReturnsTrue(string email)
{
var token = new RegistrationEmailVerificationTokenable(email, null);
Assert.True(token.TokenIsValid(email, null));
}
/// <summary>
/// Tests the token validity when the receiveMarketingEmails input is not provided
/// </summary>
[Theory, AutoData]
public void TokenIsValid_ReceiveMarketingEmailsNotProvided_ReturnsTrue(string email, string name)
{
var token = new RegistrationEmailVerificationTokenable(email, name);
Assert.True(token.TokenIsValid(email, name));
}
// TokenIsValid_IncorrectEmail_ReturnsFalse
/// <summary>
/// Tests the token validity when an incorrect email is provided
/// </summary>
[Theory, AutoData]
public void TokenIsValid_WrongEmail_ReturnsFalse(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.False(token.TokenIsValid("wrong@email.com", name, receiveMarketingEmails));
}
/// <summary>
/// Tests the token validity when an incorrect name is provided
/// </summary>
[Theory, AutoData]
public void TokenIsValid_IncorrectName_ReturnsFalse(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.False(token.TokenIsValid(email, "wrongName", receiveMarketingEmails));
}
/// <summary>
/// Tests the token validity when an incorrect receiveMarketingEmails is provided
/// </summary>
[Theory, AutoData]
public void TokenIsValid_IncorrectReceiveMarketingEmails_ReturnsFalse(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.False(token.TokenIsValid(email, name, !receiveMarketingEmails));
}
/// <summary>
/// Tests the token validity when valid inputs are provided
/// </summary>
[Theory, AutoData]
public void TokenIsValid_ValidInputs_ReturnsTrue(string email, string name, bool receiveMarketingEmails)
{
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails);
Assert.True(token.TokenIsValid(email, name, receiveMarketingEmails));
}
/// <summary>
/// Tests the deserialization of a token to ensure that the expiration date is preserved.
/// </summary>
[Theory, AutoData]
public void FromToken_SerializedToken_PreservesExpirationDate(string email, string name, bool receiveMarketingEmails)
{
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
var token = new RegistrationEmailVerificationTokenable(email, name, receiveMarketingEmails)
{
ExpirationDate = expectedDateTime
};
var result = Tokenable.FromToken<RegistrationEmailVerificationTokenable>(token.ToToken());
Assert.Equal(expectedDateTime, result.ExpirationDate, precision: _timeTolerance);
}
}