diff --git a/PlanTempus.X.BDD/Class1.cs b/PlanTempus.X.BDD/Class1.cs new file mode 100644 index 0000000..6a7a5b6 --- /dev/null +++ b/PlanTempus.X.BDD/Class1.cs @@ -0,0 +1,155 @@ +// PlanTempus.X.Services.cs +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace PlanTempus.X.Services +{ + // Models + public class User + { + public string Id { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public bool EmailConfirmed { get; set; } + public string Password { get; set; } + public bool IsLocked { get; set; } + public DateTime? LockoutEnd { get; set; } + } + + public class Organization + { + public string Id { get; set; } + public string Name { get; set; } + public string CreatedBy { get; set; } + } + + public class UserOrganization + { + public string UserId { get; set; } + public string OrganizationId { get; set; } + public string Role { get; set; } + } + + // Service interfaces + public interface IUserService + { + Task CreateUserAsync(string email, string name); + Task GetUserByEmailAsync(string email); + User GetUserByEmail(string email); + Task ConfirmEmailAsync(string confirmationToken); + } + + public interface IEmailService + { + Task GetConfirmationLinkForEmail(string email); + bool WasConfirmationEmailSent(string email); + } + + public interface IOrganizationService + { + Task SetupOrganizationAsync(string userId, string orgName, string password); + Task CreateOrganizationAsync(string userId, string orgName); + } + + public interface IUserOrganizationService + { + UserOrganization GetUserOrganization(string userId, string organizationId); + List GetUserOrganizations(string userId); + } + + public interface ITenantService + { + bool ValidateTenantTablesExist(string organizationId); + } + + public interface IAuthService + { + bool IsUserAuthenticated(string userId); + Task AttemptLoginAsync(string email, string password); + } + + // Service implementations + public class UserService : IUserService + { + public async Task CreateUserAsync(string email, string name) + { + throw new NotImplementedException("CreateUserAsync not implemented"); + } + + public async Task GetUserByEmailAsync(string email) + { + throw new NotImplementedException("GetUserByEmailAsync not implemented"); + } + + public User GetUserByEmail(string email) + { + throw new NotImplementedException("GetUserByEmail not implemented"); + } + + public async Task ConfirmEmailAsync(string confirmationToken) + { + throw new NotImplementedException("ConfirmEmailAsync not implemented"); + } + } + + public class EmailService : IEmailService + { + public async Task GetConfirmationLinkForEmail(string email) + { + throw new NotImplementedException("GetConfirmationLinkForEmail not implemented"); + } + + public bool WasConfirmationEmailSent(string email) + { + throw new NotImplementedException("WasConfirmationEmailSent not implemented"); + } + } + + public class OrganizationService : IOrganizationService + { + public async Task SetupOrganizationAsync(string userId, string orgName, string password) + { + throw new NotImplementedException("SetupOrganizationAsync not implemented"); + } + + public async Task CreateOrganizationAsync(string userId, string orgName) + { + throw new NotImplementedException("CreateOrganizationAsync not implemented"); + } + } + + public class UserOrganizationService : IUserOrganizationService + { + public UserOrganization GetUserOrganization(string userId, string organizationId) + { + throw new NotImplementedException("GetUserOrganization not implemented"); + } + + public List GetUserOrganizations(string userId) + { + throw new NotImplementedException("GetUserOrganizations not implemented"); + } + } + + public class TenantService : ITenantService + { + public bool ValidateTenantTablesExist(string organizationId) + { + throw new NotImplementedException("ValidateTenantTablesExist not implemented"); + } + } + + public class AuthService : IAuthService + { + public bool IsUserAuthenticated(string userId) + { + throw new NotImplementedException("IsUserAuthenticated not implemented"); + } + + public async Task AttemptLoginAsync(string email, string password) + { + throw new NotImplementedException("AttemptLoginAsync not implemented"); + } + } +} \ No newline at end of file diff --git a/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs b/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs index 1876a58..9ea4f6a 100644 --- a/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs +++ b/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs @@ -1,73 +1,333 @@ using LightBDD.Framework; using LightBDD.Framework.Scenarios; using LightBDD.MsTest3; +using PlanTempus.X.Services; 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 UserRegistrationSpecs : FeatureFixture { - //private CalculatorContext _context; + protected User _currentUser; + protected string _currentEmail; + protected Exception _registrationError; - public UserRegistrationSpecs() + IUserService _userService; + IEmailService _emailService; + IOrganizationService _organizationService; + + public async Task Given_no_user_exists_with_email(string email) { - //_context = new CalculatorContext(); - } - //[Scenario] - //[TestMethod] - //public void Successful_user_registration_with_valid_email() - //{ - // // Runner.RunScenario( - // // _ => Given_no_user_exists_with_email("test@example.com") - - // //); - // //await Runner.RunScenarioAsync( - // // _ => Given_no_user_exists_with_email("test@example.com"), - // // _ => When_I_submit_registration_with_name_and_email("Test User", "test@example.com"), - // // _ => Then_a_new_user_should_be_created_with_email_and_confirmation_status("test@example.com", false), - // // _ => Then_a_confirmation_email_should_be_sent() - // //); - //} - protected async Task Given_no_user_exists_with_email(string email) - { - await Task.Run(() => - { - - }); - //Assert.IsFalse(await _context.UserExistsAsync(email), $"User with email {email} should not exist"); - true.ShouldBe(true); + // Ensure user doesn't exist with email + var user = await _userService.GetUserByEmailAsync(email); + user.ShouldBeNull(); + _currentEmail = email; } - protected async Task When_I_submit_registration_with_name_and_email(string name, string email) + public async Task When_I_submit_registration_with_name_and_email(string name, string email) { - //await _context.RegisterUserAsync(name, email); - await Task.Run(() => + try { - - }); - true.ShouldBe(true); + _currentUser = await _userService.CreateUserAsync(email, name); + _currentEmail = email; + } + catch (Exception ex) + { + _registrationError = ex; + } } - protected async Task Then_a_new_user_should_be_created_with_email_and_confirmation_status(string email, bool emailConfirmed) + public async Task When_I_submit_registration_with_email(string email) { - //var user = await _context.GetUserByEmailAsync(email); - //Assert.IsNotNull(user); - //Assert.AreEqual(email, user.Email); - //Assert.AreEqual(emailConfirmed, user.EmailConfirmed); - await Task.Run(() => + try { - - }); - true.ShouldBe(true); + _currentUser = await _userService.CreateUserAsync(email, "Test User"); + _currentEmail = email; + } + catch (Exception ex) + { + _registrationError = ex; + } } - protected async Task Then_a_confirmation_email_should_be_sent() + public void Then_a_new_user_should_be_created_with_email_and_confirmation_status(string email, bool confirmationStatus) { - //Assert.IsTrue(await _context.WasConfirmationEmailSentAsync(), "Confirmation email should be sent"); - await Task.Run(() => - { + _currentUser.ShouldNotBeNull(); + _currentUser.Email.ShouldBe(email); + _currentUser.EmailConfirmed.ShouldBe(confirmationStatus); + } - }); - true.ShouldBe(true); + public void Then_a_confirmation_email_should_be_sent() + { + var emailSent = _emailService.WasConfirmationEmailSent(_currentEmail); + emailSent.ShouldBeTrue(); + } + + public async Task Given_a_user_already_exists_with_email(string email) + { + // Create a user first to ensure it exists + _currentUser = await _userService.CreateUserAsync(email, "Existing User"); + _currentUser.ShouldNotBeNull(); + _currentEmail = email; + } + + public void Then_registration_should_fail_with_error(string expectedErrorMessage) + { + _registrationError.ShouldNotBeNull(); + _registrationError.Message.ShouldBe(expectedErrorMessage); + } +} + + + +[TestClass] +[FeatureDescription(@"As a registered user +I want to confirm my email +So I can activate my account")] +public partial class EmailConfirmationSpecs : FeatureFixture +{ + IUserService _userService; + IEmailService _emailService; + IOrganizationService _organizationService; + + protected User _currentUser; + protected string _currentEmail; + protected string _confirmationLink; + protected bool _redirectedToWelcome; + protected string _errorMessage; + + public async Task Given_a_user_exists_with_unconfirmed_email(string email) + { + _currentUser = await _userService.CreateUserAsync(email, "Test User"); + _currentUser.EmailConfirmed.ShouldBeFalse(); + _currentEmail = email; + } + + public async Task When_I_click_the_valid_confirmation_link_for(string email) + { + _confirmationLink = await _emailService.GetConfirmationLinkForEmail(email); + await _userService.ConfirmEmailAsync(_confirmationLink); + _redirectedToWelcome = true; // Simulate redirect + } + + public void Then_the_users_email_confirmed_should_be_true() + { + _currentUser = _userService.GetUserByEmail(_currentEmail); + _currentUser.EmailConfirmed.ShouldBeTrue(); + } + + public void And_I_should_be_redirected_to_the_welcome_page() + { + _redirectedToWelcome.ShouldBeTrue(); + } + + public async Task When_I_click_an_invalid_confirmation_link() + { + try + { + await _userService.ConfirmEmailAsync("invalid-confirmation-token"); + } + catch (Exception ex) + { + _errorMessage = ex.Message; + } + } + + public void Then_I_should_see_an_error_message(string expectedErrorMessage) + { + _errorMessage.ShouldBe(expectedErrorMessage); + } + + public void And_my_email_remains_unconfirmed() + { + if (_currentUser != null) + { + _currentUser.EmailConfirmed.ShouldBeFalse(); + } + } +} + + +[TestClass] +[FeatureDescription(@"As a user with confirmed email +I want to set up my organization +So I can start using the system with my team")] +public partial class OrganizationSetupSpecs : FeatureFixture +{ + IUserService _userService; + IEmailService _emailService; + IOrganizationService _organizationService; + IUserOrganizationService _userOrganizationService; + ITenantService _tenantService; + IAuthService _authService; + + protected User _currentUser; + protected Organization _organization; + protected Exception _setupError; + protected List _userOrganizations; + + public async Task Given_user_has_confirmed_their_email(string email) + { + // Create a user with confirmed email + _currentUser = await _userService.CreateUserAsync(email, "Test User"); + var confirmationLink = await _emailService.GetConfirmationLinkForEmail(email); + await _userService.ConfirmEmailAsync(confirmationLink); + _currentUser.EmailConfirmed.ShouldBeTrue(); + } + + public async Task When_I_submit_organization_name_and_valid_password(string orgName, string password) + { + try + { + _organization = await _organizationService.SetupOrganizationAsync(_currentUser.Id, orgName, password); + } + catch (Exception ex) + { + _setupError = ex; + } + } + + public void Then_a_new_organization_should_be_created_with_expected_properties() + { + _organization.ShouldNotBeNull(); + _organization.Name.ShouldBe("Acme Corp"); + _organization.CreatedBy.ShouldBe(_currentUser.Id); + } + + public void And_the_user_should_be_linked_to_the_organization_in_user_organizations() + { + var userOrg = _userOrganizationService.GetUserOrganization(_currentUser.Id, _organization.Id); + userOrg.ShouldNotBeNull(); + } + + public void And_tenant_tables_should_be_created_for_the_organization() + { + var tenantTablesExist = _tenantService.ValidateTenantTablesExist(_organization.Id); + tenantTablesExist.ShouldBeTrue(); + } + + public void And_I_should_be_logged_into_the_system() + { + var isAuthenticated = _authService.IsUserAuthenticated(_currentUser.Id); + isAuthenticated.ShouldBeTrue(); + } + + public async Task When_I_submit_organization_name_without_password(string orgName) + { + try + { + await _organizationService.SetupOrganizationAsync(_currentUser.Id, orgName, ""); + } + catch (Exception ex) + { + _setupError = ex; + } + } + + public void Then_organization_setup_should_fail_with_error(string expectedErrorMessage) + { + _setupError.ShouldNotBeNull(); + _setupError.Message.ShouldBe(expectedErrorMessage); + } + + public async Task Given_user_has_completed_initial_setup(string email) + { + await Given_user_has_confirmed_their_email(email); + await When_I_submit_organization_name_and_valid_password("First Org", "ValidP@ssw0rd"); + _userOrganizations = new List { _organization }; + } + + public async Task When_I_create_a_new_organization(string orgName) + { + var newOrg = await _organizationService.CreateOrganizationAsync(_currentUser.Id, orgName); + _userOrganizations.Add(newOrg); + } + + public void Then_a_new_organization_entry_should_be_created() + { + _userOrganizations.Count.ShouldBe(2); + _userOrganizations[1].Name.ShouldBe("Second Org"); + } + + public void And_the_user_should_be_linked_to_both_organizations() + { + var userOrgs = _userOrganizationService.GetUserOrganizations(_currentUser.Id); + userOrgs.Count.ShouldBe(2); + } +} + + + + + +[TestClass] +[FeatureDescription(@"As a system administrator +I want to ensure account security is maintained +So users' data remains protected")] +public partial class AccountSecuritySpecs : FeatureFixture +{ + IUserService _userService; + IEmailService _emailService; + IOrganizationService _organizationService; + IAuthService _authService; + + protected User _currentUser; + protected DateTime? _lockoutEnd; + protected bool _isLocked; + protected bool _loginSuccessful; + + public async Task Given_user_exists(string email) + { + _currentUser = await _userService.GetUserByEmailAsync(email); + if (_currentUser == null) + { + _currentUser = await _userService.CreateUserAsync(email, "Test User"); + } + } + + public async Task When_I_attempt_5_failed_logins_within_5_minutes() + { + for (var i = 0; i < 5; i++) + { + try + { + await _authService.AttemptLoginAsync(_currentUser.Email, "WrongPassword"); + } + catch + { + // Expected exception with wrong password + } + } + } + + public void Then_the_account_should_be_locked() + { + _currentUser = _userService.GetUserByEmail(_currentUser.Email); + _isLocked = _currentUser.IsLocked; + _isLocked.ShouldBeTrue(); + } + + public void And_lockout_end_should_be_set() + { + _lockoutEnd = _currentUser.LockoutEnd; + _lockoutEnd.ShouldNotBeNull(); + _lockoutEnd.Value.ShouldBeGreaterThan(DateTime.UtcNow); + } + + public async Task And_subsequent_login_attempts_should_fail_until_lockout_end() + { + try + { + _loginSuccessful = await _authService.AttemptLoginAsync(_currentUser.Email, _currentUser.Password); + } + catch + { + _loginSuccessful = false; + } + + _loginSuccessful.ShouldBeFalse(); } } \ No newline at end of file diff --git a/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs b/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs index 4e111b7..9019e44 100644 --- a/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs +++ b/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs @@ -1,7 +1,6 @@ using LightBDD.Framework; using LightBDD.Framework.Scenarios; using LightBDD.MsTest3; - namespace PlanTempus.X.BDD.Scenarios; [TestClass] @@ -18,4 +17,103 @@ public partial class UserRegistrationSpecs : FeatureFixtures.UserRegistrationSpe _ => Then_a_confirmation_email_should_be_sent() ); } + + [Scenario] + [TestMethod] + public async Task Reject_duplicate_email_registration() + { + await Runner.RunScenarioAsync( + _ => Given_a_user_already_exists_with_email("existing@example.com"), + _ => When_I_submit_registration_with_email("existing@example.com"), + _ => Then_registration_should_fail_with_error("Email already exists") + ); + } } + + +[TestClass] +public partial class EmailConfirmationSpecs : FeatureFixtures.EmailConfirmationSpecs +{ + [Scenario] + [TestMethod] + public async Task Confirm_valid_email_address() + { + await Runner.RunScenarioAsync( + _ => Given_a_user_exists_with_unconfirmed_email("test@example.com"), + _ => When_I_click_the_valid_confirmation_link_for("test@example.com"), + _ => Then_the_users_email_confirmed_should_be_true(), + _ => And_I_should_be_redirected_to_the_welcome_page() + ); + } + + [Scenario] + [TestMethod] + public async Task Handle_invalid_confirmation_link() + { + await Runner.RunScenarioAsync( + _ => When_I_click_an_invalid_confirmation_link(), + _ => Then_I_should_see_an_error_message("Invalid confirmation link"), + _ => And_my_email_remains_unconfirmed() + ); + } +} + + +[TestClass] +public partial class OrganizationSetupSpecs : FeatureFixtures.OrganizationSetupSpecs +{ + [Scenario] + [TestMethod] + public async Task Complete_organization_setup_after_confirmation() + { + await Runner.RunScenarioAsync( + _ => Given_user_has_confirmed_their_email("test@example.com"), + _ => When_I_submit_organization_name_and_valid_password("Acme Corp", "ValidP@ssw0rd"), + _ => Then_a_new_organization_should_be_created_with_expected_properties(), + _ => And_the_user_should_be_linked_to_the_organization_in_user_organizations(), + _ => And_tenant_tables_should_be_created_for_the_organization(), + _ => And_I_should_be_logged_into_the_system() + ); + } + + [Scenario] + [TestMethod] + public async Task Prevent_organization_setup_without_password() + { + await Runner.RunScenarioAsync( + _ => Given_user_has_confirmed_their_email("test@example.com"), + _ => When_I_submit_organization_name_without_password("Acme Corp"), + _ => Then_organization_setup_should_fail_with_error("Password required") + ); + } + + [Scenario] + [TestMethod] + public async Task Handle_multiple_organization_creations() + { + await Runner.RunScenarioAsync( + _ => Given_user_has_completed_initial_setup("test@example.com"), + _ => When_I_create_a_new_organization("Second Org"), + _ => Then_a_new_organization_entry_should_be_created(), + _ => And_the_user_should_be_linked_to_both_organizations() + ); + } +} + + +[TestClass] +public partial class AccountSecuritySpecs : FeatureFixtures.AccountSecuritySpecs +{ + [Scenario] + [TestMethod] + public async Task User_lockout_after_multiple_failed_attempts() + { + await Runner.RunScenarioAsync( + _ => Given_user_exists("test@example.com"), + _ => When_I_attempt_5_failed_logins_within_5_minutes(), + _ => Then_the_account_should_be_locked(), + _ => And_lockout_end_should_be_set(), + _ => And_subsequent_login_attempts_should_fail_until_lockout_end() + ); + } +} \ No newline at end of file