Adds transactional outbox and email verification

Implements outbox pattern for reliable message delivery
Adds email verification flow with Postmark integration
Enhances account registration with secure token generation

Introduces background processing for asynchronous email sending
Implements database-level notification mechanism for message processing
This commit is contained in:
Janus C. H. Knudsen 2026-01-10 11:13:33 +01:00
parent 88812177a9
commit 54b057886c
35 changed files with 1174 additions and 358 deletions

View file

@ -1,89 +1,119 @@
using LightBDD.Framework;
using LightBDD.Framework.Scenarios;
using LightBDD.MsTest3;
using PlanTempus.X.Services;
using PlanTempus.Components.Accounts.Create;
using PlanTempus.Components.Accounts.Exceptions;
using PlanTempus.Core.CommandQueries;
using Shouldly;
namespace PlanTempus.X.BDD.FeatureFixtures;
[TestClass]
[FeatureDescription(@"As a new user
I want to register with my email
So I can start using the system")]
public partial class AccountRegistrationSpecs : FeatureFixture
public partial class AccountRegistrationSpecs : BddTestFixture
{
protected Account _currentAccount;
protected string _currentEmail;
protected Exception _registrationError;
IAccountService _accountService;
IEmailService _emailService;
IOrganizationService _organizationService;
public async Task Given_no_account_exists_with_email(string email)
{
// Ensure account doesn't exist with email
var account = await _accountService.GetAccountByEmailAsync(email);
account.ShouldBeNull();
_currentEmail = email;
}
public async Task When_I_submit_registration_with_email_and_password(string email, string password)
{
try
{
_currentAccount = await _accountService.CreateAccountAsync(email, password);
_currentEmail = email;
}
catch (Exception ex)
{
_registrationError = ex;
}
}
public async Task When_I_submit_registration_with_email(string email)
{
try
{
_currentAccount = await _accountService.CreateAccountAsync(email, "TestPassword123!");
_currentEmail = email;
}
catch (Exception ex)
{
_registrationError = ex;
}
}
public async Task Then_a_new_account_should_be_created_with_email_and_confirmation_status(string email, bool confirmationStatus)
{
_currentAccount.ShouldNotBeNull();
_currentAccount.Email.ShouldBe(email);
_currentAccount.EmailConfirmed.ShouldBe(confirmationStatus);
protected CommandResponse _commandResponse;
protected string _currentEmail;
protected Exception _registrationError;
public async Task Given_a_unique_email_address()
{
// Generate a unique email to ensure no account exists
_currentEmail = $"{GetRandomWord()}_{Guid.NewGuid():N}@test.example.com";
await Task.CompletedTask;
}
public async Task Then_a_confirmation_email_should_be_sent()
{
var emailSent = _emailService.WasConfirmationEmailSent(_currentEmail);
emailSent.ShouldBeTrue();
public async Task When_I_submit_registration_with_valid_credentials()
{
try
{
var command = new CreateAccountCommand
{
Email = _currentEmail,
Password = "TestPassword123!",
IsActive = true,
CorrelationId = Guid.NewGuid()
};
_commandResponse = await CommandHandler.Handle(command);
}
catch (Exception ex)
{
_registrationError = ex;
}
}
public async Task When_I_submit_registration_with_email(string email, string password)
{
try
{
var command = new CreateAccountCommand
{
Email = email,
Password = password,
IsActive = true,
CorrelationId = Guid.NewGuid()
};
_commandResponse = await CommandHandler.Handle(command);
_currentEmail = email;
}
catch (Exception ex)
{
_registrationError = ex;
}
}
public async Task Then_the_account_should_be_created_successfully()
{
_registrationError.ShouldBeNull();
_commandResponse.ShouldNotBeNull();
_commandResponse.RequestId.ShouldNotBe(Guid.Empty);
_commandResponse.CommandName.ShouldBe(nameof(CreateAccountCommand));
await Task.CompletedTask;
}
public async Task Given_an_account_already_exists_with_email(string email)
{
// Create an account first to ensure it exists
_currentAccount = await _accountService.CreateAccountAsync(email, "ExistingPassword123!");
_currentAccount.ShouldNotBeNull();
_currentEmail = email;
{
// Create an account first to ensure it exists
var command = new CreateAccountCommand
{
Email = email,
Password = "ExistingPassword123!",
IsActive = true,
CorrelationId = Guid.NewGuid()
};
await Task.CompletedTask;
await CommandHandler.Handle(command);
_currentEmail = email;
}
public async Task Then_registration_should_fail_with_error(string expectedErrorMessage)
{
_registrationError.ShouldNotBeNull();
_registrationError.Message.ShouldBe(expectedErrorMessage);
public async Task When_I_try_to_register_with_the_same_email()
{
try
{
var command = new CreateAccountCommand
{
Email = _currentEmail,
Password = "AnotherPassword123!",
IsActive = true,
CorrelationId = Guid.NewGuid()
};
_commandResponse = await CommandHandler.Handle(command);
}
catch (Exception ex)
{
_registrationError = ex;
}
}
public async Task Then_registration_should_fail_with_duplicate_email_error()
{
_registrationError.ShouldNotBeNull();
_registrationError.ShouldBeOfType<EmailAlreadyRegistreredException>();
await Task.CompletedTask;
}