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(); using var scope = db.CreateScope(nameof(EmailConfirmationSpecs)); var result = await scope.Connection.QuerySqlAsync( "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(); using var scope = db.CreateScope(nameof(EmailConfirmationSpecs)); var result = await scope.Connection.QuerySqlAsync( @"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(); using var scope = db.CreateScope(nameof(EmailConfirmationSpecs)); var result = await scope.Connection.QuerySqlAsync( @"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(); using var scope = db.CreateScope(nameof(EmailConfirmationSpecs)); var result = await scope.Connection.QuerySqlAsync( "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(); using var scope = db.CreateScope(nameof(EmailConfirmationSpecs)); var result = await scope.Connection.QuerySqlAsync( "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; } } }