diff --git a/.codacyignore b/.codacyignore new file mode 100644 index 00000000..64317f1f --- /dev/null +++ b/.codacyignore @@ -0,0 +1,39 @@ +# Ignore the test projects entirely +EstateManagementUI.BlazorServer.Tests/** +EstateManagementUI.IntegrationTests/** + +# Common build/IDE artifacts +bin/ +obj/ +.vs/ +.vscode/ +.idea/ +packages/ + +# Node / frontend artifacts +node_modules/ +wwwroot/lib/ + +# Generated code and designer files +**/*.g.cs +**/*.g.i.cs +**/*.designer.cs +**/*.Generated.cs +**/Generated/** + +# Coverage and test results +coverage/ +coverage.* +coverage.xml +lcov.info +TestResults/ + +# Archives / legacy folders +Archive/ +**/Archive/** + +# Other artifacts +*.user +*.suo +*.db +*.sqlite \ No newline at end of file diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/BaseTest.cs b/EstateManagementUI.BlazorServer.Tests/Pages/BaseTest.cs new file mode 100644 index 00000000..930d4ddd --- /dev/null +++ b/EstateManagementUI.BlazorServer.Tests/Pages/BaseTest.cs @@ -0,0 +1,84 @@ +using System.Security.Claims; +using Bunit; +using Bunit.TestDoubles; +using EstateManagementUI.BlazorServer.Components.Permissions; +using EstateManagementUI.BlazorServer.Permissions; +using MediatR; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace EstateManagementUI.BlazorServer.Tests.Pages; + +public abstract class BaseTest :TestContext { + protected BaseTest() { + this._mockMediator = new Mock(); + this._mockNavigationManager = new Mock(); + this._mockPermissionKeyProvider = new Mock(); + this._mockAuthStateProvider = new Mock(); + this._mockPermissionService = new Mock(); + this._mockPermissionStore = new Mock(); + this._fakeNavigationManager = new FakeNavigationManager(); + + this._mockPermissionKeyProvider.Setup(x => x.GetKey()).Returns("test-key"); + this._mockPermissionService.Setup(x => x.HasPermissionAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); + + this.Services.AddSingleton(this._mockMediator.Object); + //this.Services.AddSingleton(this._mockNavigationManager.Object); + Services.AddSingleton(_fakeNavigationManager); // register FakeNavigationManager + this.Services.AddSingleton(this._mockPermissionKeyProvider.Object); + this.Services.AddSingleton(this._mockPermissionService.Object); + this.Services.AddSingleton(this._mockAuthStateProvider.Object); + this.Services.AddSingleton(this._mockPermissionStore.Object); + + + // Add required permission components + this.ComponentFactories.AddStub(); + this.ComponentFactories.AddStub(); + + var claims = new[] { new Claim(ClaimTypes.Role, "Estate"), new Claim("estateId", Guid.NewGuid().ToString()), new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "EstateUser") }; + this.AddTestAuthorization().SetClaims(claims); + } + + protected readonly Mock _mockMediator; + protected readonly Mock _mockNavigationManager; + protected readonly Mock _mockPermissionKeyProvider; + protected readonly Mock _mockPermissionService; + protected readonly Mock _mockAuthStateProvider; + protected readonly Mock _mockPermissionStore; + protected readonly FakeNavigationManager _fakeNavigationManager; + + /// + /// Minimal test double for NavigationManager. + /// Register in DI as NavigationManager so components receive it in tests. + /// Use the or to assert navigation. + /// + public class FakeNavigationManager : NavigationManager + { + public List NavigatedUris { get; } = new(); + + public FakeNavigationManager() + { + // sensible defaults for tests + Initialize("http://localhost/", "http://localhost/"); + } + + protected override void NavigateToCore(String uri, + NavigationOptions options) { + var absolute = ToAbsoluteUri(uri).ToString(); + Uri = absolute; // protected setter on base is accessible here + NavigatedUris.Add(absolute); + } + + protected override void NavigateToCore(string uri, bool forceLoad) + { + // Ensure an absolute URI is recorded + var absolute = ToAbsoluteUri(uri).ToString(); + Uri = absolute; // protected setter on base is accessible here + NavigatedUris.Add(absolute); + } + + public string? LastUri => NavigatedUris.Count > 0 ? NavigatedUris[^1] : null; + } +} \ No newline at end of file diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/Estate/EstateIndexPageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/Estate/EstateIndexPageTests.cs index 7a023a59..4b6dd9d5 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/Estate/EstateIndexPageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/Estate/EstateIndexPageTests.cs @@ -1,15 +1,13 @@ +using AngleSharp.Dom; using Bunit; -using EstateManagementUI.BlazorServer.Permissions; +using EstateManagementUI.BlazorServer.Common; +using EstateManagementUI.BlazorServer.Tests.Pages.FileProcessing; using EstateManagementUI.BusinessLogic.Models; using EstateManagementUI.BusinessLogic.Requests; -using MediatR; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Components.Web; using Moq; using Shouldly; using SimpleResults; -using System.Security.Claims; -using EstateManagementUI.BlazorServer.Tests.Pages.FileProcessing; using EstateIndex = EstateManagementUI.BlazorServer.Components.Pages.Estate.Index; namespace EstateManagementUI.BlazorServer.Tests.Pages.Estate; @@ -20,8 +18,7 @@ public class EstateIndexPageTests : BaseTest public void EstateIndex_RendersCorrectly() { // Arrange - var estate = new EstateModel - { + EstateModel estate = new() { EstateId = Guid.NewGuid(), EstateName = "Test Estate", Reference = "EST001" @@ -39,7 +36,7 @@ public void EstateIndex_RendersCorrectly() .ReturnsAsync(Result.Success(new List())); // Act - var cut = RenderComponent(); + IRenderedComponent cut = RenderComponent(); // Assert cut.Markup.ShouldContain("Estate Management"); @@ -49,8 +46,7 @@ public void EstateIndex_RendersCorrectly() public void EstateIndex_DisplaysEstateDetails() { // Arrange - var estate = new EstateModel - { + EstateModel estate = new() { EstateId = Guid.NewGuid(), EstateName = "Test Estate", Reference = "EST001", @@ -69,7 +65,7 @@ public void EstateIndex_DisplaysEstateDetails() .ReturnsAsync(Result.Success(new List())); // Act - var cut = RenderComponent(); + IRenderedComponent cut = RenderComponent(); // Assert cut.WaitForAssertion(() => cut.Markup.ShouldContain("Test Estate"), timeout: TimeSpan.FromSeconds(5)); @@ -91,10 +87,624 @@ public void EstateIndex_HasCorrectPageTitle() .ReturnsAsync(Result.Success(new List())); // Act - var cut = RenderComponent(); + IRenderedComponent cut = RenderComponent(); // Assert - var pageTitle = cut.FindComponent(); + IRenderedComponent pageTitle = cut.FindComponent(); pageTitle.Instance.ChildContent.ShouldNotBeNull(); } + + [Fact] + public void EstateIndex_TabSwitch_ToOperators_UpdatesActiveTab() + { + // Arrange + SetupSuccessfulDataLoad(); + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Act - Find operators button and click it + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Assigned Operators"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_TabSwitch_BackToOverview_UpdatesActiveTab() + { + // Arrange + SetupSuccessfulDataLoad(); + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators first + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Act - switch back to overview + IElement? overviewButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Overview")); + overviewButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Estate Name"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_AddOperator_Success_UpdatesAssignedOperators() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorDropDownModel operatorToAdd = new() { + OperatorId = operatorId, + OperatorName = "Test Operator" + }; + + SetupSuccessfulDataLoadWithOperators(new List { operatorToAdd }); + + OperatorModel operatorDetails = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = false + }; + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(operatorDetails)); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success()); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Click Add Operator button + IElement addOperatorButton = cut.Find("#addOperatorButton"); + addOperatorButton.Click(); + + // Act - Select operator and add + IElement selectElement = cut.Find("select"); + selectElement.Change(operatorId.ToString()); + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addOperatorButton"); + addButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Operator added successfully"), timeout: TimeSpan.FromSeconds(5)); + + } + + [Fact] + public void EstateIndex_AddOperator_Failure_ShowsErrorMessage() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorDropDownModel operatorToAdd = new() { + OperatorId = operatorId, + OperatorName = "Test Operator" + }; + + SetupSuccessfulDataLoadWithOperators(new List { operatorToAdd }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to add operator")); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Click Add Operator button + IElement addOperatorButton = cut.Find("#addOperatorButton"); + addOperatorButton.Click(); + + // Act - Select operator and add + IElement selectElement = cut.Find("select"); + selectElement.Change(operatorId.ToString()); + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addOperatorButton"); + addButton.Click(); + + // Assert + _mockMediator.Verify(x => x.Send(It.IsAny(), default), Times.AtLeastOnce()); + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to add operator"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_RemoveOperator_Success_RemovesFromList() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorModel assignedOperator = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = false + }; + + SetupSuccessfulDataLoadWithAssignedOperators(new List { assignedOperator }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success()); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Act - Remove operator + IRefreshableElementCollection removeButtons = cut.FindAll("button"); + IElement? removeButton = removeButtons.FirstOrDefault(b => b.TextContent.Contains("Remove")); + removeButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Operator removed successfully"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_RemoveOperator_Failure_ShowsErrorMessage() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorModel assignedOperator = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = false + }; + + SetupSuccessfulDataLoadWithAssignedOperators(new List { assignedOperator }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to remove operator")); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Act - Remove operator + IRefreshableElementCollection removeButtons = cut.FindAll("button"); + IElement? removeButton = removeButtons.FirstOrDefault(b => b.TextContent.Contains("Remove")); + removeButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Failed to remove operator"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_DisplaysMerchants_WhenPresent() + { + // Arrange + List merchants = new() { + new RecentMerchantsModel + { + MerchantId = Guid.NewGuid(), + Name = "Test Merchant", + Reference = "MERCH001", + CreatedDateTime = new DateTime(2024, 1, 1, 12, 0, 0) + } + }; + + SetupSuccessfulDataLoadWithMerchants(merchants); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + cut.Markup.ShouldContain("Test Merchant"); + cut.Markup.ShouldContain("MERCH001"); + } + + [Fact] + public void EstateIndex_DisplaysNoMerchants_WhenEmpty() + { + // Arrange + SetupSuccessfulDataLoad(); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + cut.Markup.ShouldContain("No merchants found"); + } + + [Fact] + public void EstateIndex_DisplaysContracts_WhenPresent() + { + // Arrange + List contracts = new() { + new RecentContractModel + { + ContractId = Guid.NewGuid(), + Description = "Test Contract", + OperatorName = "Test Operator" + } + }; + + SetupSuccessfulDataLoadWithContracts(contracts); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + cut.Markup.ShouldContain("Test Contract"); + } + + [Fact] + public void EstateIndex_DisplaysNoContracts_WhenEmpty() + { + // Arrange + SetupSuccessfulDataLoad(); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + cut.Markup.ShouldContain("No contracts found"); + } + + [Fact] + public void EstateIndex_LoadEstateData_EstateQueryFails_NavigatesToError() + { + // Arrange + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to load estate")); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_LoadEstateData_MerchantQueryFails_NavigatesToError() + { + // Arrange + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new EstateModel { EstateId = Guid.NewGuid() })); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to load merchants")); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_LoadEstateData_ContractQueryFails_NavigatesToError() + { + // Arrange + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new EstateModel { EstateId = Guid.NewGuid() })); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to load contracts")); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_LoadEstateData_AssignedOperatorsQueryFails_NavigatesToError() + { + // Arrange + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new EstateModel { EstateId = Guid.NewGuid() })); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to load assigned operators")); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_LoadEstateData_AllOperatorsQueryFails_NavigatesToError() + { + // Arrange + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new EstateModel { EstateId = Guid.NewGuid() })); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to load operators")); + + // Act + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Assert + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_DisplaysOperatorRequirements_WhenPresent() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorModel assignedOperator = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = true + }; + + SetupSuccessfulDataLoadWithAssignedOperators(new List { assignedOperator }); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Act - Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Assert + cut.WaitForAssertion(() => { + cut.Markup.ShouldContain("Requires Merchant Number"); + cut.Markup.ShouldContain("Requires Terminal Number"); + }, timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_DisplaysNoOperators_WhenNoneAssigned() + { + // Arrange + SetupSuccessfulDataLoad(); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Act - Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("No operators assigned"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_AddOperator_WhenGetOperatorQueryFails_NavigatesToError() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorDropDownModel operatorToAdd = new() { + OperatorId = operatorId, + OperatorName = "Test Operator" + }; + + SetupSuccessfulDataLoadWithOperators(new List { operatorToAdd }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success()); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Failure("Failed to get operator details")); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Click Add Operator button + IElement addOperatorButton = cut.Find("#addOperatorButton"); + addOperatorButton.Click(); + + // Act - Select operator and add + IElement selectElement = cut.Find("select"); + selectElement.Change(operatorId.ToString()); + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addOperatorButton"); + addButton.Click(); + + // Assert - Should navigate to error page + _fakeNavigationManager.Uri.ShouldContain("error"); + } + + [Fact] + public void EstateIndex_AddOperator_WhenException_ShowsErrorMessage() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorDropDownModel operatorToAdd = new() { + OperatorId = operatorId, + OperatorName = "Test Operator" + }; + + SetupSuccessfulDataLoadWithOperators(new List { operatorToAdd }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ThrowsAsync(new Exception("Test exception")); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Click Add Operator button + IElement addOperatorButton = cut.Find("#addOperatorButton"); + addOperatorButton.Click(); + + // Act - Select operator and add + IElement selectElement = cut.Find("select"); + selectElement.Change(operatorId.ToString()); + IElement addButton = cut.FindAll("button") + .First(b => b.TextContent.Trim() == "Add" && (b.GetAttribute("id") ?? "") != "addOperatorButton"); + addButton.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("An error occurred: Test exception"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_RemoveOperator_WhenException_ShowsErrorMessage() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorModel assignedOperator = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = false + }; + + SetupSuccessfulDataLoadWithAssignedOperators(new List { assignedOperator }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ThrowsAsync(new Exception("Test exception")); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Act - Remove operator + IRefreshableElementCollection removeButtons = cut.FindAll("button"); + IElement? removeButton = removeButtons.FirstOrDefault(b => b.TextContent.Contains("Remove")); + removeButton?.Click(); + + // Assert + cut.WaitForAssertion(() => cut.Markup.ShouldContain("An error occurred: Test exception"), timeout: TimeSpan.FromSeconds(5)); + } + + [Fact] + public void EstateIndex_SuccessMessage_ClearsWhenSwitchingTabs() + { + // Arrange + Guid operatorId = Guid.NewGuid(); + OperatorModel assignedOperator = new() { + OperatorId = operatorId, + Name = "Test Operator", + RequireCustomMerchantNumber = true, + RequireCustomTerminalNumber = false + }; + + SetupSuccessfulDataLoadWithAssignedOperators(new List { assignedOperator }); + + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success()); + + IRenderedComponent cut = RenderComponent(); + cut.WaitForState(() => !cut.Markup.Contains("animate-spin"), TimeSpan.FromSeconds(5)); + + // Switch to operators tab + IRefreshableElementCollection buttons = cut.FindAll("button"); + IElement? operatorsButton = buttons.FirstOrDefault(b => b.TextContent.Contains("Operators")); + operatorsButton?.Click(); + + // Remove operator to trigger success message + IRefreshableElementCollection removeButtons = cut.FindAll("button"); + IElement? removeButton = removeButtons.FirstOrDefault(b => b.TextContent.Contains("Remove")); + removeButton?.Click(); + + cut.WaitForAssertion(() => cut.Markup.ShouldContain("Operator removed successfully"), timeout: TimeSpan.FromSeconds(5)); + + // Act - Switch to overview tab + IRefreshableElementCollection overviewButtons = cut.FindAll("button"); + IElement? overviewButton = overviewButtons.FirstOrDefault(b => b.TextContent.Contains("Overview")); + overviewButton?.Click(); + + // Assert - Success message should be cleared + cut.WaitForAssertion(() => cut.Markup.ShouldNotContain("Operator removed successfully"), timeout: TimeSpan.FromSeconds(5)); + } + + // Helper methods + private void SetupSuccessfulDataLoad(List? merchants = null, + List? contracts = null, + List? assignedOperators = null, + List? operators = null) + { + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(new EstateModel { EstateId = Guid.NewGuid(), EstateName = "Test Estate" })); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(merchants ?? new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(contracts ?? new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(assignedOperators ?? new List())); + _mockMediator.Setup(x => x.Send(It.IsAny(), default)) + .ReturnsAsync(Result.Success(operators ?? new List())); + } + + private void SetupSuccessfulDataLoadWithOperators(List operators) + => SetupSuccessfulDataLoad(operators: operators); + + private void SetupSuccessfulDataLoadWithAssignedOperators(List assignedOperators) + => SetupSuccessfulDataLoad(assignedOperators: assignedOperators); + + private void SetupSuccessfulDataLoadWithMerchants(List merchants) + => SetupSuccessfulDataLoad(merchants: merchants); + + private void SetupSuccessfulDataLoadWithContracts(List contracts) + => SetupSuccessfulDataLoad(contracts: contracts); } diff --git a/EstateManagementUI.BlazorServer.Tests/Pages/FileProcessing/FileProcessingIndexPageTests.cs b/EstateManagementUI.BlazorServer.Tests/Pages/FileProcessing/FileProcessingIndexPageTests.cs index 25912bb9..77a4a9e7 100644 --- a/EstateManagementUI.BlazorServer.Tests/Pages/FileProcessing/FileProcessingIndexPageTests.cs +++ b/EstateManagementUI.BlazorServer.Tests/Pages/FileProcessing/FileProcessingIndexPageTests.cs @@ -1,57 +1,14 @@ using Bunit; -using Bunit.TestDoubles; -using EstateManagementUI.BlazorServer.Components.Permissions; using EstateManagementUI.BlazorServer.Models; -using EstateManagementUI.BlazorServer.Permissions; using EstateManagementUI.BusinessLogic.Requests; -using MediatR; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.Extensions.DependencyInjection; using Moq; using Shouldly; using SimpleResults; -using System.Security.Claims; using FileImportLogModel = EstateManagementUI.BusinessLogic.Models.FileImportLogModel; using FileProcessingIndex = EstateManagementUI.BlazorServer.Components.Pages.FileProcessing.Index; namespace EstateManagementUI.BlazorServer.Tests.Pages.FileProcessing; -public abstract class BaseTest :TestContext { - protected BaseTest() { - _mockMediator = new Mock(); - _mockNavigationManager = new Mock(); - _mockPermissionKeyProvider = new Mock(); - _mockAuthStateProvider = new Mock(); - _mockPermissionService = new Mock(); - _mockPermissionStore = new Mock(); - - _mockPermissionKeyProvider.Setup(x => x.GetKey()).Returns("test-key"); - _mockPermissionService.Setup(x => x.HasPermissionAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - - Services.AddSingleton(_mockMediator.Object); - Services.AddSingleton(_mockNavigationManager.Object); - Services.AddSingleton(_mockPermissionKeyProvider.Object); - Services.AddSingleton(_mockPermissionService.Object); - Services.AddSingleton(_mockAuthStateProvider.Object); - Services.AddSingleton(_mockPermissionStore.Object); - - // Add required permission components - ComponentFactories.AddStub(); - ComponentFactories.AddStub(); - - var claims = new[] { new Claim(ClaimTypes.Role, "Estate"), new Claim("estateId", Guid.NewGuid().ToString()), new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "EstateUser") }; - this.AddTestAuthorization().SetClaims(claims); - } - - protected readonly Mock _mockMediator; - protected readonly Mock _mockNavigationManager; - protected readonly Mock _mockPermissionKeyProvider; - protected readonly Mock _mockPermissionService; - protected readonly Mock _mockAuthStateProvider; - protected readonly Mock _mockPermissionStore; -} - public class FileProcessingIndexPageTests : BaseTest { [Fact] diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Estate/Index.razor.cs b/EstateManagementUI.BlazorServer/Components/Pages/Estate/Index.razor.cs index 12bb8655..5c5c72fd 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Estate/Index.razor.cs +++ b/EstateManagementUI.BlazorServer/Components/Pages/Estate/Index.razor.cs @@ -39,6 +39,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) var result = await this.LoadEstateData(correlationId, estateId); if (result.IsFailed) { this.NavigationManager.NavigateToErrorPage(); + return; } } finally @@ -110,22 +111,16 @@ private async Task AddOperatorToEstate() ClearMessages(); - try - { + try { var correlationId = new CorrelationId(Guid.NewGuid()); Guid estateId = await this.GetEstateId(); var operatorId = Guid.Parse(selectedOperatorId); - var command = new EstateCommands.AddOperatorToEstateCommand( - correlationId, - estateId, - operatorId - ); + var command = new EstateCommands.AddOperatorToEstateCommand(correlationId, estateId, operatorId); var result = await Mediator.Send(command); - if (result.IsSuccess) - { + if (result.IsSuccess) { successMessage = "Operator added successfully"; selectedOperatorId = null; showAddOperator = false; @@ -135,26 +130,26 @@ private async Task AddOperatorToEstate() if (op != null && !assignedOperators.Any(a => a.OperatorId == operatorId)) { OperatorQueries.GetOperatorQuery query = new OperatorQueries.GetOperatorQuery(CorrelationIdHelper.New(), await this.GetEstateId(), operatorId); var operatorResult = await Mediator.Send(query); - if (operatorResult.IsFailed) + if (operatorResult.IsFailed) { this.NavigationManager.NavigateToErrorPage(); + //return; + } - assignedOperators.Add(new OperatorModel() { - OperatorId = operatorResult.Data.OperatorId, - Name = operatorResult.Data.Name, - RequireCustomMerchantNumber = operatorResult.Data.RequireCustomMerchantNumber, - RequireCustomTerminalNumber = operatorResult.Data.RequireCustomTerminalNumber - }); + assignedOperators.Add(new OperatorModel() { OperatorId = operatorResult.Data.OperatorId, Name = operatorResult.Data.Name, RequireCustomMerchantNumber = operatorResult.Data.RequireCustomMerchantNumber, RequireCustomTerminalNumber = operatorResult.Data.RequireCustomTerminalNumber }); } } - else - { + else { errorMessage = result.Message ?? "Failed to add operator"; } } - catch (Exception ex) - { + catch (Exception ex) { errorMessage = $"An error occurred: {ex.Message}"; } + finally { + // Small delay so user sees confirmation (adjust duration as needed) + await Task.Delay(2500); + this.StateHasChanged(); + } } private async Task RemoveOperatorFromEstate(Guid operatorId) diff --git a/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj b/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj index 9e8a9e46..11bb04f1 100644 --- a/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj +++ b/EstateManagementUI.BlazorServer/EstateManagementUI.BlazorServer.csproj @@ -8,6 +8,7 @@ Regular 8b7e2a4c-9f1d-4e5a-b3c6-1a8d9e7f2b4c Linux + Full