diff --git a/Database/Core/ConnectionFactory/PostgresConnectionFactory.cs b/Database/Core/ConnectionFactory/PostgresConnectionFactory.cs index b42c899..1def0ee 100644 --- a/Database/Core/ConnectionFactory/PostgresConnectionFactory.cs +++ b/Database/Core/ConnectionFactory/PostgresConnectionFactory.cs @@ -8,7 +8,7 @@ namespace PlanTempus.Database.Core.ConnectionFactory { private readonly NpgsqlDataSource _baseDataSource; private readonly Action _configureDataSource; - private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; + private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; //this is not tested nor implemented, I just created it as an idea public PostgresConnectionFactory( string connectionString, diff --git a/PlanTempus.Components/Class1.cs b/PlanTempus.Components/Class1.cs new file mode 100644 index 0000000..d850929 --- /dev/null +++ b/PlanTempus.Components/Class1.cs @@ -0,0 +1,7 @@ +namespace PlanTempus.Components +{ + public class Class1 + { + + } +} diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs new file mode 100644 index 0000000..bac9e3f --- /dev/null +++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationCommand.cs @@ -0,0 +1,9 @@ +namespace PlanTempus.Components.Organizations.Create +{ + public class CreateOrganizationCommand + { + public string Name { get; set; } + public string Description { get; set; } + public Guid CreatedById { get; set; } + } +} diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs new file mode 100644 index 0000000..9009d48 --- /dev/null +++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationHandler.cs @@ -0,0 +1,52 @@ +using Insight.Database; + +namespace PlanTempus.Components.Organizations.Create +{ + public class CreateOrganizationHandler + { + private readonly IDatabaseOperations _databaseOperations; + + public CreateOrganizationHandler(IDatabaseOperations databaseOperations) + { + _databaseOperations = databaseOperations; + } + + public async Task Handle(CreateOrganizationCommand command) + { + using var db = _databaseOperations.CreateScope(nameof(CreateOrganizationHandler)); + try + { + var organizationId = Guid.NewGuid(); + var now = DateTime.UtcNow; + + var sql = @" + INSERT INTO Organizations (Id, Name, Description, CreatedById, CreatedAt, UpdatedAt) + VALUES (@Id, @Name, @Description, @CreatedById, @CreatedAt, @UpdatedAt)"; + + await db.Connection.ExecuteSqlAsync(sql, new + { + Id = organizationId, + command.Name, + command.Description, + CreatedById = command.CreatedById, + CreatedAt = now, + UpdatedAt = now + }); + + db.Success(); + + return new CreateOrganizationResponse + { + Id = organizationId, + Name = command.Name, + CreatedAt = now + }; + } + catch (Exception ex) + { + db.Error(ex); + throw; + } + } + } +} \ No newline at end of file diff --git a/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs b/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs new file mode 100644 index 0000000..c181f9d --- /dev/null +++ b/PlanTempus.Components/Organizations/Create/CreateOrganizationResponse.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PlanTempus.Components.Organizations.Create +{ + public class CreateOrganizationResponse + { + public Guid Id { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + } +} diff --git a/PlanTempus.Components/PlanTempus.Components.csproj b/PlanTempus.Components/PlanTempus.Components.csproj new file mode 100644 index 0000000..eb495fd --- /dev/null +++ b/PlanTempus.Components/PlanTempus.Components.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + + + + + + + + + + + + diff --git a/PlanTempus.X.BDD/FeatureFixtures/AccountSecuritySpecs.cs b/PlanTempus.X.BDD/FeatureFixtures/AccountSecuritySpecs.cs new file mode 100644 index 0000000..dfb04c7 --- /dev/null +++ b/PlanTempus.X.BDD/FeatureFixtures/AccountSecuritySpecs.cs @@ -0,0 +1,81 @@ +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 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 async Task Then_the_account_should_be_locked() + { + _currentUser = _userService.GetUserByEmail(_currentUser.Email); + _isLocked = _currentUser.IsLocked; + _isLocked.ShouldBeTrue(); + + await Task.CompletedTask; + + } + + public async Task And_lockout_end_should_be_set() + { + _lockoutEnd = _currentUser.LockoutEnd; + _lockoutEnd.ShouldNotBeNull(); + _lockoutEnd.Value.ShouldBeGreaterThan(DateTime.UtcNow); + + await Task.CompletedTask; + + } + + 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/FeatureFixtures/EmailConfirmationSpecs.cs b/PlanTempus.X.BDD/FeatureFixtures/EmailConfirmationSpecs.cs new file mode 100644 index 0000000..4e2bf49 --- /dev/null +++ b/PlanTempus.X.BDD/FeatureFixtures/EmailConfirmationSpecs.cs @@ -0,0 +1,73 @@ +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 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 async Task Then_the_users_email_confirmed_should_be_true() + { + _currentUser = _userService.GetUserByEmail(_currentEmail); + _currentUser.EmailConfirmed.ShouldBeTrue(); + } + + public async Task 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 async Task Then_I_should_see_an_error_message(string expectedErrorMessage) + { + _errorMessage.ShouldBe(expectedErrorMessage); + } + + public async Task And_my_email_remains_unconfirmed() + { + if (_currentUser != null) + { + _currentUser.EmailConfirmed.ShouldBeFalse(); + } + } +} \ No newline at end of file diff --git a/PlanTempus.X.BDD/FeatureFixtures/OrganizationSetupSpecs.cs b/PlanTempus.X.BDD/FeatureFixtures/OrganizationSetupSpecs.cs new file mode 100644 index 0000000..e59536f --- /dev/null +++ b/PlanTempus.X.BDD/FeatureFixtures/OrganizationSetupSpecs.cs @@ -0,0 +1,134 @@ +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 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_user_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 async Task Then_a_new_organization_should_be_created_with_expected_properties() + { + _organization.ShouldNotBeNull(); + _organization.Name.ShouldBe("Acme Corp"); + _organization.CreatedBy.ShouldBe(_currentUser.Id); + + await Task.CompletedTask; + } + + public async Task And_the_user_should_be_linked_to_the_organization_in_user_organizations() + { + var userOrg = _userOrganizationService.GetUserOrganization(_currentUser.Id, _organization.Id); + userOrg.ShouldNotBeNull(); + + await Task.CompletedTask; + + } + + public async Task And_tenant_tables_should_be_created_for_the_organization() + { + var tenantTablesExist = _tenantService.ValidateTenantTablesExist(_organization.Id); + tenantTablesExist.ShouldBeTrue(); + + await Task.CompletedTask; + + } + + public async Task And_user_should_be_logged_into_the_system() + { + var isAuthenticated = _authService.IsUserAuthenticated(_currentUser.Id); + isAuthenticated.ShouldBeTrue(); + + await Task.CompletedTask; + + } + + public async Task When_user_submit_organization_name_without_password(string orgName) + { + try + { + await _organizationService.SetupOrganizationAsync(_currentUser.Id, orgName, ""); + } + catch (Exception ex) + { + _setupError = ex; + } + } + + public async Task Then_organization_setup_should_fail_with_error(string expectedErrorMessage) + { + _setupError.ShouldNotBeNull(); + _setupError.Message.ShouldBe(expectedErrorMessage); + + await Task.CompletedTask; + + } + + public async Task Given_user_has_completed_initial_setup(string email) + { + await Given_user_has_confirmed_their_email(email); + await When_user_submit_organization_name_and_valid_password("First Org", "ValidP@ssw0rd"); + _userOrganizations = new List { _organization }; + } + + public async Task When_user_create_a_new_organization(string orgName) + { + var newOrg = await _organizationService.CreateOrganizationAsync(_currentUser.Id, orgName); + _userOrganizations.Add(newOrg); + } + + public async Task Then_a_new_organization_entry_should_be_created() + { + _userOrganizations.Count.ShouldBe(2); + _userOrganizations[1].Name.ShouldBe("Second Org"); + + await Task.CompletedTask; + + } + + public async Task And_the_user_should_be_linked_to_both_organizations() + { + var userOrgs = _userOrganizationService.GetUserOrganizations(_currentUser.Id); + userOrgs.Count.ShouldBe(2); + + await Task.CompletedTask; + + } +} \ No newline at end of file diff --git a/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs b/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs index 9ea4f6a..2c822db 100644 --- a/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs +++ b/PlanTempus.X.BDD/FeatureFixtures/UserRegistrationSpecs.cs @@ -53,281 +53,42 @@ public partial class UserRegistrationSpecs : FeatureFixture } } - public void Then_a_new_user_should_be_created_with_email_and_confirmation_status(string email, bool confirmationStatus) + public async Task Then_a_new_user_should_be_created_with_email_and_confirmation_status(string email, bool confirmationStatus) { _currentUser.ShouldNotBeNull(); _currentUser.Email.ShouldBe(email); _currentUser.EmailConfirmed.ShouldBe(confirmationStatus); - } - public void Then_a_confirmation_email_should_be_sent() + await Task.CompletedTask; + + } + + public async Task 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) + await Task.CompletedTask; + + } + + 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) + await Task.CompletedTask; + + } + + public async Task Then_registration_should_fail_with_error(string expectedErrorMessage) { _registrationError.ShouldNotBeNull(); _registrationError.Message.ShouldBe(expectedErrorMessage); - } + + await Task.CompletedTask; + + } } - - - -[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/AccountSecuritySpecs.cs b/PlanTempus.X.BDD/Scenarios/AccountSecuritySpecs.cs new file mode 100644 index 0000000..e446c18 --- /dev/null +++ b/PlanTempus.X.BDD/Scenarios/AccountSecuritySpecs.cs @@ -0,0 +1,21 @@ +using LightBDD.Framework; +using LightBDD.Framework.Scenarios; +using LightBDD.MsTest3; + +namespace PlanTempus.X.BDD.Scenarios; + [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 diff --git a/PlanTempus.X.BDD/Scenarios/EmailConfirmationSpecs.cs b/PlanTempus.X.BDD/Scenarios/EmailConfirmationSpecs.cs new file mode 100644 index 0000000..2565735 --- /dev/null +++ b/PlanTempus.X.BDD/Scenarios/EmailConfirmationSpecs.cs @@ -0,0 +1,31 @@ +using LightBDD.Framework; +using LightBDD.Framework.Scenarios; +using LightBDD.MsTest3; + +namespace PlanTempus.X.BDD.Scenarios; + [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() + ); + } +} \ No newline at end of file diff --git a/PlanTempus.X.BDD/Scenarios/OrganizationSetupSpecs.cs b/PlanTempus.X.BDD/Scenarios/OrganizationSetupSpecs.cs new file mode 100644 index 0000000..4c82cee --- /dev/null +++ b/PlanTempus.X.BDD/Scenarios/OrganizationSetupSpecs.cs @@ -0,0 +1,45 @@ +using LightBDD.Framework; +using LightBDD.Framework.Scenarios; +using LightBDD.MsTest3; + +namespace PlanTempus.X.BDD.Scenarios; + [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_user_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_user_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_user_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_user_create_a_new_organization("Second Org"), + _ => Then_a_new_organization_entry_should_be_created(), + _ => And_the_user_should_be_linked_to_both_organizations() + ); + } +} \ No newline at end of file diff --git a/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs b/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs index 9019e44..3138b6f 100644 --- a/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs +++ b/PlanTempus.X.BDD/Scenarios/UserRegistrationSpecs.cs @@ -29,91 +29,3 @@ public partial class UserRegistrationSpecs : FeatureFixtures.UserRegistrationSpe ); } } - - -[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 diff --git a/PlanTempus.sln b/PlanTempus.sln index dc63cfc..93ae5de 100644 --- a/PlanTempus.sln +++ b/PlanTempus.sln @@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.SetupInfrastruct EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanTempus.X.BDD", "PlanTempus.X.BDD\PlanTempus.X.BDD.csproj", "{8CA2246B-7D8C-40DA-9042-CA17A7A7672B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanTempus.Components", "PlanTempus.Components\PlanTempus.Components.csproj", "{ECC8621A-7B3F-4E26-85A1-926FA263E5D7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,10 +34,6 @@ Global {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Debug|Any CPU.Build.0 = Debug|Any CPU {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Release|Any CPU.ActiveCfg = Release|Any CPU {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Release|Any CPU.Build.0 = Release|Any CPU - {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Release|Any CPU.Build.0 = Release|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -44,6 +42,14 @@ Global {48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.Build.0 = Release|Any CPU + {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CA2246B-7D8C-40DA-9042-CA17A7A7672B}.Release|Any CPU.Build.0 = Release|Any CPU + {ECC8621A-7B3F-4E26-85A1-926FA263E5D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECC8621A-7B3F-4E26-85A1-926FA263E5D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECC8621A-7B3F-4E26-85A1-926FA263E5D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECC8621A-7B3F-4E26-85A1-926FA263E5D7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SqlManagement/.dbeaver/.project-metadata.json.bak b/SqlManagement/.dbeaver/.project-metadata.json.bak index 3200961..c8ced2b 100644 --- a/SqlManagement/.dbeaver/.project-metadata.json.bak +++ b/SqlManagement/.dbeaver/.project-metadata.json.bak @@ -1 +1 @@ -{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"}}} \ No newline at end of file +{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44"}}} \ No newline at end of file diff --git a/SqlManagement/.dbeaver/project-metadata.json b/SqlManagement/.dbeaver/project-metadata.json index 525a28a..3200961 100644 --- a/SqlManagement/.dbeaver/project-metadata.json +++ b/SqlManagement/.dbeaver/project-metadata.json @@ -1 +1 @@ -{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-schema":"system","default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"sandbox"}}} \ No newline at end of file +{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/Script.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"},"Scripts/SmartConfigSystem.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptmain"}}} \ No newline at end of file diff --git a/Tests/ConfigurationSystem/SetupConfigurationTests.cs b/Tests/ConfigurationSystem/SetupConfigurationTests.cs index 085575a..1894e9d 100644 --- a/Tests/ConfigurationSystem/SetupConfigurationTests.cs +++ b/Tests/ConfigurationSystem/SetupConfigurationTests.cs @@ -2,8 +2,8 @@ using Insight.Database; using System.Data; using Newtonsoft.Json; -using FluentAssertions; using Autofac; +using Shouldly; using PlanTempus.Database.Core.ConnectionFactory; namespace PlanTempus.Tests.ConfigurationSystem; @@ -64,7 +64,7 @@ public class SetupConfigurationTests : TestFixture action_type = "I" }); var actual = JsonConvert.SerializeObject(history) as string; - actual.Should().Be(expected); + actual.ShouldBe(expected); } [TestMethod] @@ -95,7 +95,7 @@ public class SetupConfigurationTests : TestFixture .Single(); // Assert - ((DateTime)updated.modified_at).Should().BeAfter((DateTime)original.modified_at); + ((DateTime)updated.modified_at).ShouldBeGreaterThan((DateTime)original.modified_at); } [TestMethod] @@ -134,7 +134,7 @@ public class SetupConfigurationTests : TestFixture action_type = "D" }); var actual = JsonConvert.SerializeObject(history) as string; - actual.Should().Be(expected); + actual.ShouldBe(expected); } [TestMethod]