PlanTempusApp/PlanTempus.X.BDD/FeatureFixtures/EmailConfirmationSpecs.cs
Janus C. H. Knudsen 54b057886c 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
2026-01-10 11:13:33 +01:00

164 lines
4.5 KiB
C#

using Autofac;
using Insight.Database;
using LightBDD.Framework;
using LightBDD.Framework.Scenarios;
using LightBDD.MsTest3;
using PlanTempus.Components;
using PlanTempus.Components.Accounts.ConfirmEmail;
using PlanTempus.Components.Accounts.Create;
using PlanTempus.Core.Database;
using PlanTempus.Core.Outbox;
using Shouldly;
namespace PlanTempus.X.BDD.FeatureFixtures;
[TestClass]
[FeatureDescription(@"As a registered user
I want to confirm my email
So I can activate my account")]
public partial class EmailConfirmationSpecs : BddTestFixture
{
protected string _currentEmail;
protected string _securityStamp;
protected bool _emailConfirmed;
protected string _errorMessage;
protected bool _outboxEntryCreated;
public async Task Given_an_account_exists_with_unconfirmed_email(string email)
{
_currentEmail = email;
var command = new CreateAccountCommand
{
CorrelationId = Guid.NewGuid(),
Email = email,
Password = "TestPassword123!"
};
await CommandHandler.Handle(command);
// Hent security_stamp fra database til brug i confirmation
var db = Container.Resolve<IDatabaseOperations>();
using var scope = db.CreateScope(nameof(EmailConfirmationSpecs));
var result = await scope.Connection.QuerySqlAsync<AccountDto>(
"SELECT email_confirmed, security_stamp FROM system.accounts WHERE email = @Email",
new { Email = email });
result.Count.ShouldBe(1);
result[0].EmailConfirmed.ShouldBeFalse();
_securityStamp = result[0].SecurityStamp;
}
public async Task And_a_verification_email_is_queued_in_outbox()
{
var db = Container.Resolve<IDatabaseOperations>();
using var scope = db.CreateScope(nameof(EmailConfirmationSpecs));
var result = await scope.Connection.QuerySqlAsync<OutboxDto>(
@"SELECT type, payload FROM system.outbox
WHERE type = @Type AND payload->>'Email' = @Email",
new { Type = OutboxMessageTypes.VerificationEmail, Email = _currentEmail });
result.Count.ShouldBeGreaterThan(0);
_outboxEntryCreated = true;
}
public async Task And_the_outbox_message_is_processed()
{
// Vent på at OutboxListener når at behandle beskeden
await Task.Delay(1000);
var db = Container.Resolve<IDatabaseOperations>();
using var scope = db.CreateScope(nameof(EmailConfirmationSpecs));
var result = await scope.Connection.QuerySqlAsync<OutboxDto>(
@"SELECT status FROM system.outbox
WHERE type = @Type AND payload->>'Email' = @Email",
new { Type = OutboxMessageTypes.VerificationEmail, Email = _currentEmail });
result.Count.ShouldBeGreaterThan(0);
// Status skal være 'sent' eller 'failed' - begge indikerer at beskeden blev behandlet
result[0].Status.ShouldBeOneOf("sent", "failed");
}
public async Task When_I_confirm_email_with_valid_token()
{
var command = new ConfirmEmailCommand
{
CorrelationId = Guid.NewGuid(),
Email = _currentEmail,
Token = _securityStamp
};
await CommandHandler.Handle(command);
}
public async Task When_I_confirm_email_with_invalid_token()
{
try
{
var command = new ConfirmEmailCommand
{
CorrelationId = Guid.NewGuid(),
Email = _currentEmail ?? "unknown@example.com",
Token = "invalid-token"
};
await CommandHandler.Handle(command);
}
catch (InvalidTokenException ex)
{
_errorMessage = ex.Message;
}
}
public async Task Then_the_accounts_email_should_be_confirmed()
{
var db = Container.Resolve<IDatabaseOperations>();
using var scope = db.CreateScope(nameof(EmailConfirmationSpecs));
var result = await scope.Connection.QuerySqlAsync<AccountDto>(
"SELECT email_confirmed FROM system.accounts WHERE email = @Email",
new { Email = _currentEmail });
result.Count.ShouldBe(1);
result[0].EmailConfirmed.ShouldBeTrue();
}
public async Task Then_I_should_see_an_error_message(string expectedMessage)
{
_errorMessage.ShouldNotBeNull();
_errorMessage.ShouldContain(expectedMessage);
}
public async Task And_the_email_remains_unconfirmed()
{
if (_currentEmail == null) return;
var db = Container.Resolve<IDatabaseOperations>();
using var scope = db.CreateScope(nameof(EmailConfirmationSpecs));
var result = await scope.Connection.QuerySqlAsync<AccountDto>(
"SELECT email_confirmed FROM system.accounts WHERE email = @Email",
new { Email = _currentEmail });
if (result.Count > 0)
{
result[0].EmailConfirmed.ShouldBeFalse();
}
}
private class AccountDto
{
public bool EmailConfirmed { get; set; }
public string SecurityStamp { get; set; }
}
private class OutboxDto
{
public string Type { get; set; }
public string Payload { get; set; }
public string Status { get; set; }
}
}