diff --git a/HopFrame.sln.DotSettings.user b/HopFrame.sln.DotSettings.user index ea6ada4..1e30ef8 100644 --- a/HopFrame.sln.DotSettings.user +++ b/HopFrame.sln.DotSettings.user @@ -36,6 +36,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor index 1d9a61e..8e0f1e1 100644 --- a/src/HopFrame.Web/Pages/Administration/AdminLogin.razor +++ b/src/HopFrame.Web/Pages/Administration/AdminLogin.razor @@ -43,7 +43,7 @@ private UserLogin UserLogin { get; set; } [SupplyParameterFromQuery(Name = "redirect")] - private string RedirectAfter { get; set; } + public string RedirectAfter { get; set; } private const string DefaultRedirect = "/administration"; diff --git a/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj b/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj index 062ade0..b6e4aa4 100644 --- a/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj +++ b/tests/HopFrame.Tests.Web/HopFrame.Tests.Web.csproj @@ -10,6 +10,7 @@ + diff --git a/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs new file mode 100644 index 0000000..0e2909a --- /dev/null +++ b/tests/HopFrame.Tests.Web/Pages/AdminLoginTests.cs @@ -0,0 +1,123 @@ +using BlazorStrap; +using Bunit; +using Bunit.TestDoubles; +using CurrieTechnologies.Razor.SweetAlert2; +using HopFrame.Security.Models; +using HopFrame.Web.Pages.Administration; +using HopFrame.Web.Services; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace HopFrame.Tests.Web.Pages; + +public class AdminLoginTests : TestContext { + + private (IRenderedComponent, NavigationManager) SetupEnvironment(bool correctCredentials = true) { + var auth = new Mock(); + auth + .Setup(a => a.Login(It.IsAny())) + .ReturnsAsync(correctCredentials); + + Services.AddSweetAlert2(); + Services.AddBlazorStrap(); + Services.AddSingleton(auth.Object); + var navigator = Services.GetRequiredService(); + + var component = RenderComponent(); + return (component, navigator); + } + + [Fact] + public void Login_Has_RequiredFields() { + // Arrange + var (component, _) = SetupEnvironment(); + + // Act + var inputs = component.FindAll("input"); + var buttons = component.FindAll("button"); + var form = component.FindAll("form"); + + // Assert + Assert.Equal(2, inputs.Count); + Assert.Single(buttons); + Assert.Single(form); + Assert.Equal("submit", buttons[0].Attributes.GetNamedItem("type")?.Value); + + foreach (var input in inputs) { + var attribute = input.Attributes.GetNamedItem("required"); + Assert.NotNull(attribute); + Assert.NotEqual("false", attribute?.Value); + } + } + + [Fact] + public void Login_With_CorrectCredentials_Should_Redirect() { + // Arrange + var (component, nav) = SetupEnvironment(); + var email = component.Find("""input[type="email"]"""); + var password = component.Find("""input[type="password"]"""); + var submit = component.Find("button"); + + // Act + email.Change("test@example.com"); + password.Change("1234567890"); + submit.Click(); + + // Assert + Assert.EndsWith("/administration", nav.Uri); + } + + [Fact] + public void Login_With_CorrectCredentials_And_CustomRedirect_Should_Redirect() { + // Arrange + var (component, nav) = SetupEnvironment(); + var email = component.Find("""input[type="email"]"""); + var password = component.Find("""input[type="password"]"""); + var submit = component.Find("button"); + + component.Instance.RedirectAfter = "testRedirect"; + + // Act + email.Change("test@example.com"); + password.Change("1234567890"); + submit.Click(); + + // Assert + Assert.EndsWith("/administration/testRedirect", nav.Uri); + } + + [Fact] + public void Login_With_IncorrectCredentials_Should_Fail() { + // Arrange + var (component, nav) = SetupEnvironment(false); + var email = component.Find("""input[type="email"]"""); + var password = component.Find("""input[type="password"]"""); + var submit = component.Find("button"); + + // Act + email.Change("test@example.com"); + password.Change("1234567890"); + submit.Click(); + + // Assert + Assert.False(nav.Uri.EndsWith("/administration")); + } + + [Fact] + public void Login_With_IncorrectCredentials_DisplaysError() { + // Arrange + var (component, _) = SetupEnvironment(false); + var email = component.Find("""input[type="email"]"""); + var password = component.Find("""input[type="password"]"""); + var submit = component.Find("button"); + + // Act + email.Change("test@example.com"); + password.Change("1234567890"); + submit.Click(); + + // Assert + Assert.Contains("Email or password does not match any account!", component.Markup); + } +} \ No newline at end of file diff --git a/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs b/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs new file mode 100644 index 0000000..92b9611 --- /dev/null +++ b/tests/HopFrame.Tests.Web/Pages/AuthorizedViewTests.cs @@ -0,0 +1,133 @@ +using System.Security.Claims; +using Bunit; +using Bunit.TestDoubles; +using HopFrame.Security.Authentication; +using HopFrame.Security.Claims; +using HopFrame.Web.Components; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace HopFrame.Tests.Web.Pages; + +public class AuthorizedViewTests : TestContext { + private readonly string _testRedirect = "testRedirect"; + private readonly string _testPermission = "test.permission"; + private readonly string _innerHtml = "

Inner Render

"; + + public NavigationManager SetupEnvironment(bool authenticated = true, params string[] userPermissions) { + var auth = new Mock(); + auth + .Setup(a => a.IsAuthenticated) + .Returns(authenticated); + + var context = new DefaultHttpContext(); + var claims = userPermissions?.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)).ToList(); + context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication.SchemeName)); + var accessor = new Mock(); + accessor + .Setup(a => a.HttpContext) + .Returns(context); + + Services.AddSingleton(auth.Object); + Services.AddSingleton(accessor.Object); + return Services.GetRequiredService(); + } + + [Fact] + public void AuthorizedView_With_NoValidLogin_And_Redirection_Should_Redirect() { + // Arrange + var navigator = SetupEnvironment(false); + + // Act + RenderComponent(parameters => parameters + .Add(a => a.RedirectIfUnauthorized, _testRedirect)); + + // Assert + Assert.EndsWith(_testRedirect, navigator.Uri); + } + + [Fact] + public void AuthorizedView_With_NoPermissions_And_Redirection_Should_Redirect() { + // Arrange + var navigator = SetupEnvironment(); + + // Act + RenderComponent(parameters => parameters + .Add(a => a.RedirectIfUnauthorized, _testRedirect) + .Add(a => a.Permission, _testPermission)); + + // Assert + Assert.EndsWith(_testRedirect, navigator.Uri); + } + + [Fact] + public void AuthorizedView_With_FewPermissions_And_Redirection_Should_Redirect() { + // Arrange + var navigator = SetupEnvironment(true, "other.permission"); + + // Act + RenderComponent(parameters => parameters + .Add(a => a.RedirectIfUnauthorized, _testRedirect) + .Add(a => a.Permissions, [_testPermission, "other.permission"])); + + // Assert + Assert.EndsWith(_testRedirect, navigator.Uri); + } + + [Fact] + public void AuthorizedView_With_Permissions_And_Redirection_Should_NotRedirect() { + // Arrange + var navigator = SetupEnvironment(true, _testPermission); + + // Act + RenderComponent(parameters => parameters + .Add(a => a.RedirectIfUnauthorized, _testRedirect) + .Add(a => a.Permission, _testPermission)); + + // Assert + Assert.False(navigator.Uri.EndsWith(_testRedirect)); + } + + [Fact] + public void AuthorizedView_With_AllPermissions_And_Redirection_Should_NotRedirect() { + // Arrange + var navigator = SetupEnvironment(true, _testPermission, "other.permission"); + + // Act + RenderComponent(parameters => parameters + .Add(a => a.RedirectIfUnauthorized, _testRedirect) + .Add(a => a.Permissions, [_testPermission, "other.permission"])); + + // Assert + Assert.False(navigator.Uri.EndsWith(_testRedirect)); + } + + [Fact] + public void AuthorizedView_With_ChildComponent_And_ValidLogin_Should_DisplayChildren() { + // Arrange + SetupEnvironment(); + + // Act + var component = RenderComponent(parameters => parameters + .AddChildContent(_innerHtml)); + + // Assert + Assert.Contains(_innerHtml, component.Markup); + } + + [Fact] + public void AuthorizedView_With_ChildComponent_And_InvalidLogin_Should_NotDisplayChildren() { + // Arrange + SetupEnvironment(false); + + // Act + var component = RenderComponent(parameters => parameters + .AddChildContent(_innerHtml)); + + // Assert + Assert.DoesNotContain(_innerHtml, component.Markup); + } + +} \ No newline at end of file