From ab43a39368b200fcdd523fd056dce258aab2eaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= <78308169+mckaragoz@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:52:48 +0300 Subject: [PATCH 1/3] test(identity-lab): add comprehensive bUnit page render coverage --- ...Beam.UltimateAuth.IdentityLab.Tests.csproj | 2 + .../README.md | 18 +- .../PageRenderContext.ApplicationPages.cs | 135 ++++++++++++ .../Rendering/PageRenderContext.cs | 144 +++++++++++++ .../Rendering/PageRenderTests.cs | 154 ++++++++++++++ .../Rendering/RemainingPageRenderTests.cs | 192 ++++++++++++++++++ .../Rendering/RouteSmokeCoverageTests.cs | 59 ++++++ .../docs/page-render-coverage.md | 143 +++++++++++++ 8 files changed, 845 insertions(+), 2 deletions(-) create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.ApplicationPages.cs create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RouteSmokeCoverageTests.cs create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/docs/page-render-coverage.md diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj index a33d9b29..8adc52da 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj @@ -11,6 +11,8 @@ + + diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md index c5afa29d..20985388 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/README.md @@ -1,12 +1,12 @@ # Identity Lab security suite -Requires .NET 10 SDK and Docker with permission to start containers. Run from the repository root: +The security and integration tests require .NET 10 SDK and Docker with permission to start containers. Run from the repository root: ```sh dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests -c Release --logger trx ``` -By default, each database test class starts PostgreSQL 17 through Testcontainers. Every test gets a new database, applies the actual LabDbContext migrations, and drops its database on disposal. Tests never use EF InMemory. Nothing connects to application production databases. +By default, each database test class starts PostgreSQL 17 through Testcontainers. Every test gets a new database, applies the actual LabDbContext migrations, and drops its database on disposal. Security and integration tests never use EF InMemory. Nothing connects to application production databases. Alternatively set `IDENTITYLAB_TEST_POSTGRES` to a connection string for a **dedicated test PostgreSQL instance**. Its login needs CREATE DATABASE and permission to drop the databases it creates. Only databases named `identitylab_test_` are created/dropped. This setting bypasses Docker; it does not bypass relational tests. Missing infrastructure fails fixture initialization rather than silently skipping tests. @@ -15,3 +15,17 @@ Tests replace authenticated-context sources and selected framework store/client `Regression/SystemRoleIdentityTests.cs` records an expected rejection that the inspected implementation does not enforce. It is intentionally neither skipped nor inverted to accept unsafe behavior. The `KnownDefect=SystemRoleIdentity` trait identifies it; normal local and CI runs include it. Expect that regression to fail until separately approved production work addresses the issue. Do not enable this suite as a required green check before defect triage. See [coverage matrix](docs/security-coverage.md) and [execution report](docs/execution-report.md). The project is included in the solution. Repository CI excludes it from the general matrix test step and runs it in a dedicated .NET 10 PostgreSQL job; the dedicated job uploads TRX results even on failure. + +## Page render suite + +The `Rendering` tests use bUnit and need only the .NET 10 SDK; no Docker or PostgreSQL is required: + +```sh +dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release --filter Category=Render +``` + +The real pages and child components render with MudBlazor services and a popover provider. External client calls use explicit Moq responses. Dashboard initialization uses real Lab services with a uniquely named EF InMemory LabDbContext store; this verifies component initialization and navigation branches, not relational behavior. Existing service regression tests and their PostgreSQL fixtures are unchanged. Direct page rendering does not exercise router authorization or layouts. + +All 24 routable application page components have smoke test mappings (28 page-render cases and one route inventory guard). New pages need a real render test annotated with `[SmokeTestFor(typeof(PageComponent))]`; the route guard reports missing types and their route aliases. Existing behavioral render cases are preserved. No additional dependencies were introduced by the completion phase. + +See [page inventory and validation limits](docs/page-render-coverage.md). diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.ApplicationPages.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.ApplicationPages.cs new file mode 100644 index 00000000..5f994ee9 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.ApplicationPages.cs @@ -0,0 +1,135 @@ +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client.Diagnostics; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.Core.Runtime; +using CodeBeam.UltimateAuth.IdentityLab.Authorization; +using CodeBeam.UltimateAuth.IdentityLab.Contracts; +using CodeBeam.UltimateAuth.IdentityLab.Services; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Users.Contracts; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace CodeBeam.UltimateAuth.IdentityLab.Tests.Rendering; + +internal sealed partial class PageRenderContext +{ + public static UserKey RenderUser => UserKey.FromString("render-user"); + + public void AddManagementServices() + { + AddLabServices(); + var summaries = new Mock(MockBehavior.Strict); + summaries.Setup(x => x.GetByKeysAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new[] { new UserSummary + { + UserKey = RenderUser, DisplayName = "Render Person", UserName = "render-person", + PrimaryEmail = "render@example.test", Status = UserStatus.Active + } }); + var summaryFactory = new Mock(MockBehavior.Strict); + summaryFactory.Setup(x => x.Create(TenantKeys.Single)).Returns(summaries.Object); + Services.AddSingleton(summaryFactory.Object); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + var product = new Mock(MockBehavior.Strict); + product.Setup(x => x.Get()).Returns(new UAuthProductInfo { Version = "render-test" }); + Services.AddSingleton(product.Object); + } + + public void SetupProfileAndAccess() + { + Users.Setup(x => x.GetMeAsync(It.IsAny())) + .ReturnsAsync(Success(new UserView + { + UserKey = RenderUser, DisplayName = "Render Person", UserName = "render-person", + PrimaryEmail = "render@example.test", Status = UserStatus.Active + })); + Identifiers.Setup(x => x.GetMyAsync(It.IsAny())) + .ReturnsAsync(Success(Empty())); + Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())) + .ReturnsAsync(Success(Empty())); + Authorization.Setup(x => x.GetMyRolesAsync(It.IsAny())) + .ReturnsAsync(Success(new UserRolesGetResult { UserKey = RenderUser, Roles = Empty() })); + Authorization.Setup(x => x.GetMyPermissionsAsync()) + .ReturnsAsync(Success(new UserPermissionsResult { Permissions = Array.Empty() })); + } + + public void AddAdminServices() + { + AddManagementServices(); + var role = Role.Create(LabRoleIds.Admin, TenantKeys.Single, "Ladmin", null, DateTimeOffset.UtcNow); + var roles = new Mock(MockBehavior.Strict); + roles.Setup(x => x.GetByIdsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new[] { role }); + var roleFactory = new Mock(MockBehavior.Strict); + roleFactory.Setup(x => x.Create(TenantKeys.Single)).Returns(roles.Object); + var assignments = new Mock(MockBehavior.Strict); + assignments.Setup(x => x.GetAssignmentsAsync(RenderUser, It.IsAny())) + .ReturnsAsync(new[] { new UserRole { Tenant = TenantKeys.Single, UserKey = RenderUser, RoleId = role.Id } }); + var assignmentFactory = new Mock(MockBehavior.Strict); + assignmentFactory.Setup(x => x.Create(TenantKeys.Single)).Returns(assignments.Object); + Services.AddSingleton(roleFactory.Object); + Services.AddSingleton(assignmentFactory.Object); + Services.AddSingleton(Mock.Of()); + Services.AddScoped(); + Services.AddScoped(); + } + + public void AddDemoStoreServices() + { + AddLabServices(); + var flow = new Mock(MockBehavior.Strict); + flow.Setup(x => x.BeginAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Contexts.AuthenticatedFlow(RenderUser)); + var contexts = new Mock(MockBehavior.Strict); + contexts.Setup(x => x.CreateAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(Contexts.Create(LabDemoPermissions.ProductsRead, RenderUser)); + Services.AddSingleton(flow.Object); + Services.AddSingleton(contexts.Object); + // An active sandbox member without demo role assignments is a valid empty-access state. + var assignments = new Mock(MockBehavior.Strict); + assignments.Setup(x => x.GetAssignmentsAsync(RenderUser, It.IsAny())) + .ReturnsAsync(Array.Empty()); + var factory = new Mock(MockBehavior.Strict); + factory.Setup(x => x.Create(TenantKeys.Single)).Returns(assignments.Object); + Services.AddSingleton(factory.Object); + var roles = new Mock(MockBehavior.Strict); + roles.Setup(x => x.GetByIdsAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(Array.Empty()); + var roleFactory = new Mock(MockBehavior.Strict); + roleFactory.Setup(x => x.Create(TenantKeys.Single)).Returns(roles.Object); + Services.AddSingleton(roleFactory.Object); + Services.AddScoped(); + } + + public void AddDiagnosticsServices() + { + Services.AddSingleton(new UAuthClientDiagnostics()); + Services.AddScoped(); + var chainId = SessionChainId.New(); + var sessionId = AuthSessionId.Parse(new string('s', 32)); + var now = DateTimeOffset.UtcNow; + var session = new SessionInfo(sessionId, now.AddMinutes(-1), now.AddHours(1), false); + var chain = new SessionChainSummary + { + ChainId = chainId, ActiveSessionId = sessionId, IsCurrentDevice = true, + State = SessionChainState.Active, CreatedAt = session.CreatedAt, LastSeenAt = now + }; + Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())) + .ReturnsAsync(Success(new PagedResult(new[] { chain }, 1, 1, 20, null, false))); + Sessions.Setup(x => x.GetMyChainDetailAsync(chainId)) + .ReturnsAsync(Success(new SessionChainDetail + { + ChainId = chainId, ActiveSessionId = sessionId, State = SessionChainState.Active, + CreatedAt = session.CreatedAt, LastSeenAt = now, Sessions = new[] { session } + })); + } +} diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs new file mode 100644 index 00000000..98733895 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs @@ -0,0 +1,144 @@ +using System.Reflection; +using System.Security.Claims; +using Bunit; +using CodeBeam.UltimateAuth.Authorization; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Client.Blazor.Extensions; +using CodeBeam.UltimateAuth.Client.Options; +using CodeBeam.UltimateAuth.Client.Services; +using CodeBeam.UltimateAuth.Core.Abstractions; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.Core.Domain; +using CodeBeam.UltimateAuth.Core.MultiTenancy; +using CodeBeam.UltimateAuth.IdentityLab.Authorization; +using CodeBeam.UltimateAuth.IdentityLab.Domain; +using CodeBeam.UltimateAuth.IdentityLab.Persistence; +using CodeBeam.UltimateAuth.IdentityLab.Services; +using CodeBeam.UltimateAuth.Server.Auth; +using CodeBeam.UltimateAuth.Server.Options; +using CodeBeam.UltimateAuth.Server.Services; +using CodeBeam.UltimateAuth.Users.Contracts; +using CodeBeam.UltimateAuth.Users.Reference; +using Microsoft.AspNetCore.Components; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using MudBlazor; +using MudBlazor.Services; +using MudExtensions.Services; + +namespace CodeBeam.UltimateAuth.IdentityLab.Tests.Rendering; + +internal sealed partial class PageRenderContext : BunitContext +{ + public Mock Client { get; } = new(MockBehavior.Strict); + public Mock Sessions { get; } = new(MockBehavior.Strict); + public Mock Authorization { get; } = new(MockBehavior.Strict); + public Mock Identifiers { get; } = new(MockBehavior.Strict); + public Mock Users { get; } = new(MockBehavior.Strict); + + public PageRenderContext() + { + // MudBlazor's DOM measurement/focus calls have no browser in bUnit. + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddLogging(); + Services.AddMudServices(); + Services.AddMudExtensions(); + Services.AddAuthorizationCore(); + // Keep the real internal evaluator, without installing the production client host. + var registrations = new ServiceCollection(); + registrations.AddUltimateAuthClientBlazor(); + ((IServiceCollection)Services).Add(registrations.Single(x => x.ServiceType == typeof(IUAuthAuthorizationEvaluator))); + Client.SetupGet(x => x.Sessions).Returns(Sessions.Object); + Client.SetupGet(x => x.Authorization).Returns(Authorization.Object); + Client.SetupGet(x => x.Users).Returns(Users.Object); + Client.SetupGet(x => x.Identifiers).Returns(Identifiers.Object); + Services.AddSingleton(Client.Object); + Services.AddSingleton(Options.Create(new UAuthClientOptions())); + Services.AddSingleton(Options.Create(new UAuthServerOptions())); + var device = new Mock(); + device.Setup(x => x.GetOrCreateAsync(It.IsAny())) + .Returns(new ValueTask(DeviceId.Create("render-test-device-0001"))); + Services.AddSingleton(device.Object); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + } + + public IRenderedComponent RenderPage(UAuthState? state = null) where T : IComponent + { + // Register page-specific dependencies before the first render freezes DI. + Render(); + var authState = state ?? UAuthState.Anonymous(); + return Render>>(p => p + .Add(x => x.Value, Task.FromResult(new Microsoft.AspNetCore.Components.Authorization.AuthenticationState(authState.ToClaimsPrincipal()))) + .AddChildContent>(child => child + .Add(x => x.Value, authState) + .AddChildContent())).FindComponent(); + } + + public static UAuthState Authenticated(string role, params string[] permissions) + { + var state = UAuthState.Anonymous(); + // Use the actual snapshot lifecycle; no production friend assembly or API changes. + typeof(UAuthState).GetMethod("ApplySnapshot", BindingFlags.Instance | BindingFlags.NonPublic)! + .Invoke(state, [new AuthStateSnapshot + { + Identity = new AuthIdentitySnapshot + { + UserKey = UserKey.FromString("render-user"), + Tenant = TenantKeys.Single, + SessionState = SessionState.Active, + UserStatus = UserStatus.Active + }, + Claims = ClaimsSnapshot.From(new[] { (ClaimTypes.Role, role) } + .Concat(permissions.Select(permission => ("uauth:permission", permission)))) + }, DateTimeOffset.UtcNow]); + return state; + } + + public void AddLabServices() + { + // Only LabDbContext uses this isolated store. No server host, migrations or PostgreSQL. + var databaseName = Guid.NewGuid().ToString(); + Services.AddDbContextFactory(o => o.UseInMemoryDatabase(databaseName)); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddSingleton(Mock.Of()); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + Services.AddScoped(); + } + + public async Task SeedSandboxAsync(bool owner) + { + await using var db = await Services.GetRequiredService>().CreateDbContextAsync(); + var sandbox = new Sandbox + { + Name = "Render sandbox", + OwnerUserKey = UserKey.FromString(owner ? "render-user" : "other-owner"), + Settings = new SandboxSettings { MaxUsers = 5 } + }; + sandbox.Members.Add(new SandboxMember + { + UserKey = UserKey.FromString("render-user"), + Role = owner ? SandboxMemberRole.Owner : SandboxMemberRole.Member + }); + db.Sandboxes.Add(sandbox); + await db.SaveChangesAsync(); + } + + public static UAuthResult Success(T value) => new() { IsSuccess = true, Status = 200, Value = value }; + public static PagedResult Empty() => new(Array.Empty(), 0, 1, 20, null, false); +} diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs new file mode 100644 index 00000000..e0ab26a1 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs @@ -0,0 +1,154 @@ +using Bunit; +using CodeBeam.UltimateAuth.Authorization.Contracts; +using CodeBeam.UltimateAuth.Client.Blazor; +using CodeBeam.UltimateAuth.Core.Contracts; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages.Dashboard; +using CodeBeam.UltimateAuth.Users.Contracts; +using Moq; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.IdentityLab.Tests.Rendering; + +[Trait("Category", "Render")] +public sealed class PageRenderTests +{ + [Fact] + [SmokeTestFor(typeof(LoginPage))] + public void Anonymous_login_renders_the_real_login_form() + { + using var context = new PageRenderContext(); + var page = context.RenderPage(); + + page.WaitForAssertion(() => + { + Assert.Contains("Welcome back.", page.Find("h1").TextContent); + Assert.NotNull(page.FindComponent().Find("form")); + Assert.Contains("Email or Username", page.Markup); + }); + } + + [Fact] + public void Authenticated_login_renders_workspace_entry_instead_of_form() + { + using var context = new PageRenderContext(); + var page = context.RenderPage(PageRenderContext.Authenticated("Owner")); + + page.WaitForAssertion(() => + { + Assert.Contains("Enter your workspace", page.Markup); + Assert.Empty(page.FindComponents()); + }); + } + + [Fact] + [SmokeTestFor(typeof(RegisterPage))] + public void Anonymous_registration_renders_without_an_existing_session() + { + using var context = new PageRenderContext(); + context.AddLabServices(); + var page = context.RenderPage(); + + page.WaitForAssertion(() => + { + Assert.Equal("Create your sandbox.", page.Find("h1").TextContent.Trim()); + Assert.NotNull(page.FindComponent()); + Assert.NotEmpty(page.FindAll("input")); + }); + } + + [Fact] + [SmokeTestFor(typeof(LandingPage))] + public void Landing_page_renders_registration_entry() + { + using var context = new PageRenderContext(); + var page = context.RenderPage(); + Assert.Contains(page.FindAll("a"), link => + link.GetAttribute("href") == "/register" && link.TextContent.Contains("Create Your Sandbox")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [SmokeTestFor(typeof(DashboardPage))] + public async Task Dashboard_renders_owner_and_member_navigation(bool owner) + { + using var context = new PageRenderContext(); + context.AddLabServices(); + context.Users.Setup(x => x.GetMeAsync(It.IsAny())) + .ReturnsAsync(PageRenderContext.Success(new UserView { DisplayName = "Render Person" })); + context.Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())) + .ReturnsAsync(PageRenderContext.Success(PageRenderContext.Empty())); + context.Authorization.Setup(x => x.GetMyRolesAsync(It.IsAny())) + .ReturnsAsync(PageRenderContext.Success(new UserRolesGetResult + { + UserKey = CodeBeam.UltimateAuth.Core.Domain.UserKey.FromString("render-user"), + Roles = PageRenderContext.Empty() + })); + await context.SeedSandboxAsync(owner); + + var page = context.RenderPage(PageRenderContext.Authenticated(owner ? "Owner" : "User")); + page.WaitForAssertion(() => + { + Assert.Contains("Your identity workspace.", page.Find("h1").TextContent); + Assert.Contains("Render sandbox", page.Markup); + Assert.Contains("Render Person", page.Markup); + Assert.Equal(owner, page.FindAll("a").Any(x => x.GetAttribute("href") == "/dashboard/users")); + Assert.Equal(owner, page.FindAll("a").Any(x => x.GetAttribute("href") == "/dashboard/settings")); + }); + context.Users.Verify(x => x.GetMeAsync(It.IsAny()), Times.Once); + context.Sessions.Verify(x => x.GetMyChainsAsync(It.IsAny()), Times.Once); + context.Authorization.Verify(x => x.GetMyRolesAsync(It.IsAny()), Times.Once); + } + + [Fact] + [SmokeTestFor(typeof(MySessionsPage))] + public void Sessions_render_empty_service_result() + { + using var context = new PageRenderContext(); + context.Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())) + .ReturnsAsync(PageRenderContext.Success(PageRenderContext.Empty())); + var page = context.RenderPage(PageRenderContext.Authenticated("User")); + + page.WaitForAssertion(() => Assert.Contains("No session chains were returned.", page.Markup)); + Assert.Equal("My Sessions", page.Find("h1").TextContent.Trim()); + context.Sessions.Verify(x => x.GetMyChainsAsync(It.IsAny()), Times.Once); + } + + [Fact] + public void Sessions_render_loading_then_explicit_service_failure() + { + using var context = new PageRenderContext(); + var response = new TaskCompletionSource>>( + TaskCreationOptions.RunContinuationsAsynchronously); + context.Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())).Returns(response.Task); + var page = context.RenderPage(PageRenderContext.Authenticated("User")); + Assert.NotEmpty(page.FindComponents()); + + response.SetResult(new UAuthResult> { IsSuccess = false, Status = 503 }); + page.WaitForAssertion(() => + { + Assert.Contains("Unable to load session chains.", page.Markup); + Assert.Empty(page.FindComponents()); + }); + } + + [Theory] + [InlineData(false, "No roles available.")] + [InlineData(true, "Authorization service is unavailable.")] + [SmokeTestFor(typeof(AuthorizationPage))] + public void Authorization_management_renders_empty_and_failure_states(bool failure, string expected) + { + using var context = new PageRenderContext(); + var query = context.Authorization.Setup(x => x.QueryRolesAsync(It.IsAny())); + if (failure) + query.ThrowsAsync(new HttpRequestException("Simulated unavailable service")); + else + query.ReturnsAsync(PageRenderContext.Success(PageRenderContext.Empty())); + + var page = context.RenderPage(PageRenderContext.Authenticated("Owner")); + page.WaitForAssertion(() => Assert.Contains(expected, page.Markup)); + Assert.Equal("Authorization Management", page.Find("h1").TextContent.Trim()); + context.Authorization.Verify(x => x.QueryRolesAsync(It.IsAny()), Times.Once); + } +} diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs new file mode 100644 index 00000000..499c2e3c --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs @@ -0,0 +1,192 @@ +using Bunit; +using CodeBeam.UltimateAuth.Core.Defaults; +using AuthorizationLabPage = CodeBeam.UltimateAuth.IdentityLab.Components.Pages.Dashboard.AuthorizationLab; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages.AuthorizationLab; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages.Dashboard; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace CodeBeam.UltimateAuth.IdentityLab.Tests.Rendering; + +[Trait("Category", "Render")] +public sealed class RemainingPageRenderTests +{ + [Fact, SmokeTestFor(typeof(UsersPage))] + public async Task Owner_can_render_sandbox_users() + { + using var context = new PageRenderContext(); + context.AddManagementServices(); + await context.SeedSandboxAsync(owner: true); + var page = AssertPage(context, "Users", "Owner"); + page.WaitForAssertion(() => Assert.Contains("Render Person", page.Markup)); + } + + [Fact, SmokeTestFor(typeof(MyIdentityPage))] + public async Task User_can_render_profile_identifiers_and_credentials() + { + using var context = new PageRenderContext(); + context.AddManagementServices(); + context.SetupProfileAndAccess(); + await context.SeedSandboxAsync(owner: false); + var page = AssertPage(context, "My Identity", "User"); + page.WaitForAssertion(() => + { + Assert.Contains("Render Person", page.Markup); + Assert.Contains(page.FindAll("h2"), heading => heading.TextContent == "Credentials & Security"); + }); + } + + [Fact, SmokeTestFor(typeof(MyAccessPage))] + public async Task User_can_render_access_with_no_assigned_roles_or_permissions() + { + using var context = new PageRenderContext(); + context.AddManagementServices(); + context.SetupProfileAndAccess(); + await context.SeedSandboxAsync(owner: false); + var page = AssertPage(context, "My Access", "User"); + page.WaitForAssertion(() => + { + Assert.Contains("No roles assigned", page.Markup); + Assert.Contains("No permissions assigned", page.Markup); + Assert.DoesNotContain("Unable to load sandbox members.", page.Markup); + }); + } + + [Fact, SmokeTestFor(typeof(SettingsPage))] + public async Task Owner_can_render_sandbox_settings() + { + using var context = new PageRenderContext(); + context.AddManagementServices(); + await context.SeedSandboxAsync(owner: true); + var page = AssertPage(context, "Sandbox Settings", "Owner"); + page.WaitForAssertion(() => Assert.Contains("Render Person", page.Markup)); + } + + [Fact, SmokeTestFor(typeof(ActivityPage))] + public async Task User_can_render_empty_sandbox_activity() + { + using var context = new PageRenderContext(); + context.AddLabServices(); + await context.SeedSandboxAsync(owner: false); + var page = AssertPage(context, "Activity", "User"); + page.WaitForAssertion(() => Assert.Contains("No activity recorded", page.Markup)); + } + + [Fact, SmokeTestFor(typeof(AdminPage))] + public void Ladmin_can_render_administration_and_repair_panel() + { + using var context = new PageRenderContext(); + context.AddAdminServices(); + var page = AssertPage(context, "Sandbox Management", "Ladmin"); + page.WaitForAssertion(() => + { + Assert.NotNull(page.FindComponent()); + Assert.Contains("No sandboxes found.", page.Markup); + }); + } + + [Fact, SmokeTestFor(typeof(SessionDiagnostics))] + public async Task User_can_render_current_session_diagnostics() + { + await using var context = new PageRenderContext(); + context.AddDiagnosticsServices(); + var page = AssertPage(context, "Session Diagnostics", "User"); + page.WaitForAssertion(() => Assert.Contains("Session active", page.Markup)); + } + + [Fact, SmokeTestFor(typeof(DemoStorePage))] + public async Task User_can_render_store_without_demo_role_grants() + { + using var context = new PageRenderContext(); + context.AddDemoStoreServices(); + await context.SeedSandboxAsync(owner: false); + var page = AssertPage(context, "Demo Store", "User"); + page.WaitForAssertion(() => Assert.Contains("Your Demo Access", page.Markup)); + } + + [Fact, SmokeTestFor(typeof(AuthorizationLabPage))] + public void User_can_render_authorization_lab_index() + { + using var context = new PageRenderContext(); + AssertPage(context, "Authorization Lab", "User"); + } + + [Fact, SmokeTestFor(typeof(Error))] + public void Error_page_renders_without_a_request_context() + { + using var context = new PageRenderContext(); + AssertPage(context, "Error."); + } + + [Fact, SmokeTestFor(typeof(PublicPage))] + public void Public_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Public Access"); + } + + [Fact, SmokeTestFor(typeof(AuthenticatedPage))] + public void Authenticated_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Authenticated Page", "User"); + } + + [Fact, SmokeTestFor(typeof(AdminRolePage))] + public void Admin_role_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Admin Role", "Admin"); + } + + [Fact, SmokeTestFor(typeof(AdminPermissionPage))] + public void Admin_permission_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Admin Permission", "Admin", adminPermission: true); + } + + [Fact, SmokeTestFor(typeof(AspNetAuthorizePage))] + public void Aspnet_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "ASP.NET Core Authorize", "User"); + } + + [Fact, SmokeTestFor(typeof(CombinedAuthorizationPage))] + public void Combined_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Combined Authorization", "Admin", adminPermission: true); + } + + [Fact, SmokeTestFor(typeof(StateViewPage))] + public void State_view_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "UAuthStateView", "Admin", authorizationExamples: true); + } + + [Fact, SmokeTestFor(typeof(MatchModesPage))] + public void Match_modes_authorization_sample_renders() + { + using var context = new PageRenderContext(); + AssertPage(context, "Authorization Match Modes", "Admin", authorizationExamples: true); + } + + private static IRenderedComponent AssertPage(PageRenderContext context, string expected, string? role = null, bool authorizationExamples = false, bool adminPermission = false) + where T : IComponent + { + var page = context.RenderPage(role is null ? null : PageRenderContext.Authenticated(role, adminPermission ? new[] { UAuthActions.Users.QueryAdmin } : Array.Empty())); + page.WaitForAssertion(() => + { + Assert.Contains(expected, page.Markup); + // A heading must not mask initialization falling into an error fallback. + // These two sample pages deliberately show denied authorization examples. + if (!authorizationExamples) + Assert.DoesNotContain(page.FindComponents(), alert => alert.Instance.Severity == Severity.Error); + }); + return page; + } +} diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RouteSmokeCoverageTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RouteSmokeCoverageTests.cs new file mode 100644 index 00000000..981cd2fa --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RouteSmokeCoverageTests.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using CodeBeam.UltimateAuth.IdentityLab.Components.Pages; +using Microsoft.AspNetCore.Components; + +namespace CodeBeam.UltimateAuth.IdentityLab.Tests.Rendering; + +/// Connects a page to the executable render test that exercises it. +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +internal sealed class SmokeTestForAttribute(Type componentType) : Attribute +{ + public Type ComponentType { get; } = componentType; +} + +[Trait("Category", "Render")] +public sealed class RouteSmokeCoverageTests +{ + [Fact] + public void Every_identity_lab_route_component_has_an_executable_smoke_test() + { + // Inspect the application assembly only. External/shared components and the + // test assembly cannot become route candidates. A multi-route page is one + // component; include all its aliases in any failure message. + var pages = typeof(LoginPage).Assembly.GetTypes() + .Where(type => !type.IsAbstract && typeof(IComponent).IsAssignableFrom(type)) + .Select(type => new + { + Type = type, + Routes = type.GetCustomAttributes(inherit: false) + .Select(route => route.Template).OrderBy(route => route).ToArray() + }) + .Where(page => page.Routes.Length > 0) + .ToDictionary(page => page.Type, page => page.Routes); + Assert.NotEmpty(pages); + + var mappings = typeof(PageRenderTests).Assembly.GetTypes() + .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + .SelectMany(method => method.GetCustomAttributes() + .Select(attribute => (Method: method, Page: attribute.ComponentType))) + .ToArray(); + Assert.NotEmpty(mappings); + foreach (var mapping in mappings) + { + var fact = mapping.Method.GetCustomAttribute(); // Includes TheoryAttribute. + Assert.True(fact is not null && fact.Skip is null, + $"{mapping.Method.DeclaringType?.Name}.{mapping.Method.Name} must be an enabled xUnit render test."); + Assert.True(pages.ContainsKey(mapping.Page), + $"Smoke mapping references a non-routable Identity Lab component: {mapping.Page.FullName}."); + } + + var tested = mappings.Select(mapping => mapping.Page).ToHashSet(); + var missing = pages.Where(page => !tested.Contains(page.Key)) + .OrderBy(page => page.Key.FullName) + .Select(page => $"{page.Key.FullName}: {string.Join(", ", page.Value)}") + .ToArray(); + Assert.True(missing.Length == 0, + "Identity Lab pages missing render smoke tests:\n" + string.Join("\n", missing) + + "\nAdd a real render test and annotate it with [SmokeTestFor(typeof(PageComponent))]."); + } +} diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/docs/page-render-coverage.md b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/docs/page-render-coverage.md new file mode 100644 index 00000000..7f0d71e3 --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/docs/page-render-coverage.md @@ -0,0 +1,143 @@ +# Identity Lab route smoke coverage + +## Baseline + +The initial ten render cases are preserved. The checkout was fast-forwarded from +`45dcfcd` to the latest available local `origin/dev`, `b886b94` (critical service +workflow regressions, #132). `HEAD...origin/dev` reports `0 0`. The merged service +regression tests are unchanged by this implementation. + +`git fetch origin dev` still fails because the configured proxy at port 8080 is +unreachable. The live remote tip could not be verified; this is based on the +available local dev ref, not a claim of live synchronization. + +## Inventory + +There are **24 routable page components with 24 route attributes** in the supplied +application source. All 24 have an actual render test mapping; there are no +routable exclusions. Counts are statically checked, not executed test results. + +| Component | Route | Render scenario | +| --- | --- | --- | +| LandingPage | `/` | Existing public registration entry | +| LoginPage | `/login` | Existing anonymous form and authenticated entry | +| RegisterPage | `/register` | Existing anonymous form | +| DashboardPage | `/dashboard` | Existing Owner and User navigation | +| MySessionsPage | `/dashboard/sessions` | Existing empty, loading and failure states | +| AuthorizationPage | `/dashboard/authorization` | Existing empty and failure states | +| UsersPage | `/dashboard/users` | Owner, active sandbox, one member summary | +| MyIdentityPage | `/dashboard/identity` | User, populated profile, empty identifiers, credential section | +| MyAccessPage | `/dashboard/access` | User, sandbox membership, empty roles and permissions | +| SettingsPage | `/dashboard/settings` | Owner, sandbox settings and resolved owner name | +| ActivityPage | `/dashboard/activity` | User, active sandbox, no activity | +| AdminPage | `/admin` | Ladmin with matching server role assignment, empty platform, real repair panel | +| SessionDiagnostics | `/dashboard/session-diagnostics` | User, current chain and active session details | +| DemoStorePage | `/dashboard/demo-store` | User, active sandbox, no demo role grants | +| AuthorizationLab | `/dashboard/authorization-lab` | User, real navigation index | +| Error | `/Error` | No HTTP request context | +| PublicPage | `/dashboard/auth/public` | Anonymous sample | +| AuthenticatedPage | `/dashboard/auth/authenticated` | User sample | +| AdminRolePage | `/dashboard/auth/admin` | Admin sample role | +| AdminPermissionPage | `/dashboard/auth/permission-admin` | Sample Admin plus query permission | +| AspNetAuthorizePage | `/dashboard/auth/aspnet` | User, ASP.NET authentication cascade | +| CombinedAuthorizationPage | `/dashboard/auth/combined` | Sample Admin plus query permission | +| StateViewPage | `/dashboard/auth/state-view` | Real shared state-view examples | +| MatchModesPage | `/dashboard/auth/match-modes` | Real shared match-mode examples | + +Credentials have no separate route; their section belongs to MyIdentityPage. +AdminRepairPanel has no route and is exercised as an actual AdminPage child. +Its scan/repair actions are outside smoke scope. Other non-routable children, +layouts and dialogs are not independently inventoried. + +## Tests and route enforcement + +The original eight test methods / ten cases retain their bodies and InlineData; +only six SmokeTestFor annotations were added to connect their pages to inventory. +Added 18 one-scenario page tests and one route coverage guard: **28 page-render +cases plus one inventory test**, 29 total cases under `Category=Render`. + +SmokeTestFor attributes live on actual enabled xUnit render methods. The guard +reflects only the Identity Lab application assembly for RouteAttribute-bearing +IComponent types. It checks every component, reports all route aliases for missing +components, rejects mappings to non-routable/external types, and verifies mappings +point to enabled Fact/Theory methods. Non-routable children and test/library +assemblies are not route candidates. A page with multiple aliases needs one real +component render test. There are no exception lists or arbitrary dynamic renders. + +The small generic assertion helper renders a statically selected real component. +It checks stable page text and rejects error alerts. StateView/MatchModes samples +are the narrow exception to the error-alert check because denied examples are +intentional content. Application pages additionally assert loaded content where +needed: member display name, profile/credentials, owner name, empty access/activity, +admin table/repair panel, active session and evaluated demo access. + +## Setup + +Reuse the original bUnit/MudBlazor/MudExtensions fixture, real UltimateAuth +authorization evaluator and uniquely named EF InMemory LabDbContext store. +Additional registrations are grouped for management, admin, demo and diagnostics; +external client/store responses are explicit. Real page and Lab service lifecycle +methods remain active. No application host or child stubs are installed. + +The renderer now supplies ASP.NET AuthenticationState as well as UAuthState. +The existing internal snapshot adapter accepts optional permission claims. +Regular application Users do not receive admin permission claims. The sample +routes use their declared sample Admin role/permission as appropriate. +Diagnostics renders a real current-chain/detail response and uses async fixture +disposal for its timer/subscription lifecycle. No new packages or database +dependencies were added in this completion phase. + +## Validation + +Attempted from repository root: + +```sh +git fetch origin dev +dotnet build tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release +dotnet test tests/CodeBeam.UltimateAuth.IdentityLab.Tests/CodeBeam.UltimateAuth.IdentityLab.Tests.csproj -c Release --filter Category=Render +``` + +Fetch failed: configured proxy unreachable. Both .NET commands failed before +execution: `dotnet: command not found`. **No build, render test, route guard or +existing service regression test has executed here.** No measured coverage is +reported. + +Completed static checks: + +- Compared and fast-forwarded to available local dev; no divergent commits. +- Matched all 24 source page types/routes to the annotated actual render methods. +- Reviewed component namespaces, cascades, injected service constructors, client + and store method signatures, result constructors and bUnit API usage against + existing repository conventions. This cannot substitute for compilation. +- Parsed project XML and checked added-source whitespace. +- `git diff --check` passed. +- Full patch, including new files, passes `git apply --cached --check` against + an isolated index initialized from `b886b94`. + +On a .NET 10 CI runner, the filtered command above runs the existing behavioral +render cases, new page smoke cases and route guard without Docker, PostgreSQL or a +browser. Run the full Identity Lab project separately with its documented +PostgreSQL/Docker requirements. Existing known-defect tests remain unchanged. + +## Limits + +Compilation and successful rendering remain unverified until .NET execution. +These tests bypass routing, so they do not validate authorization enforcement by +the router. Layout composition, dialogs, mutations, repair scans, populated large +data sets and detailed interaction states remain outside this smoke phase. EF +InMemory does not validate relational constraints, SQL translation, migrations or +transactions. No percentage coverage target or estimate is used. + +## Files + +Completion phase: + +- Rendering/PageRenderContext.cs: identifier client, ASP.NET cascade, optional claims. +- Rendering/PageRenderContext.ApplicationPages.cs: focused dependency/response groups. +- Rendering/PageRenderTests.cs: mappings only; original cases preserved. +- Rendering/RemainingPageRenderTests.cs: 18 additional real page smoke tests. +- Rendering/RouteSmokeCoverageTests.cs: metadata mapping and route guard. +- README.md and this report: inventory, commands and validation limits. + +The prior phase's project dependencies remain bunit 2.11.3 and EF InMemory 10.0.12. +No production code, migration, CI configuration or service test was edited. From 6e25f6cef52fa44d1c68917e2fd2b51822916b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 1 Nov 2026 00:09:03 +0300 Subject: [PATCH 2/3] Fix Tests --- .../Rendering/PageRenderContext.cs | 4 +- .../Rendering/PageRenderTests.cs | 30 ++++----- .../Rendering/RemainingPageRenderTests.cs | 61 +++++++++---------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs index 98733895..0c59e9bf 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderContext.cs @@ -94,8 +94,8 @@ public static UAuthState Authenticated(string role, params string[] permissions) SessionState = SessionState.Active, UserStatus = UserStatus.Active }, - Claims = ClaimsSnapshot.From(new[] { (ClaimTypes.Role, role) } - .Concat(permissions.Select(permission => ("uauth:permission", permission)))) + Claims = ClaimsSnapshot.From(new[] { (Type: ClaimTypes.Role, Value: role) } + .Concat(permissions.Select(permission => (Type: "uauth:permission", Value: permission))).ToArray()) }, DateTimeOffset.UtcNow]); return state; } diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs index e0ab26a1..0bf5feda 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/PageRenderTests.cs @@ -15,9 +15,9 @@ public sealed class PageRenderTests { [Fact] [SmokeTestFor(typeof(LoginPage))] - public void Anonymous_login_renders_the_real_login_form() + public async Task Anonymous_login_renders_the_real_login_form() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); var page = context.RenderPage(); page.WaitForAssertion(() => @@ -29,9 +29,9 @@ public void Anonymous_login_renders_the_real_login_form() } [Fact] - public void Authenticated_login_renders_workspace_entry_instead_of_form() + public async Task Authenticated_login_renders_workspace_entry_instead_of_form() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); var page = context.RenderPage(PageRenderContext.Authenticated("Owner")); page.WaitForAssertion(() => @@ -43,9 +43,9 @@ public void Authenticated_login_renders_workspace_entry_instead_of_form() [Fact] [SmokeTestFor(typeof(RegisterPage))] - public void Anonymous_registration_renders_without_an_existing_session() + public async Task Anonymous_registration_renders_without_an_existing_session() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddLabServices(); var page = context.RenderPage(); @@ -59,9 +59,9 @@ public void Anonymous_registration_renders_without_an_existing_session() [Fact] [SmokeTestFor(typeof(LandingPage))] - public void Landing_page_renders_registration_entry() + public async Task Landing_page_renders_registration_entry() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); var page = context.RenderPage(); Assert.Contains(page.FindAll("a"), link => link.GetAttribute("href") == "/register" && link.TextContent.Contains("Create Your Sandbox")); @@ -73,7 +73,7 @@ public void Landing_page_renders_registration_entry() [SmokeTestFor(typeof(DashboardPage))] public async Task Dashboard_renders_owner_and_member_navigation(bool owner) { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddLabServices(); context.Users.Setup(x => x.GetMeAsync(It.IsAny())) .ReturnsAsync(PageRenderContext.Success(new UserView { DisplayName = "Render Person" })); @@ -103,9 +103,9 @@ public async Task Dashboard_renders_owner_and_member_navigation(bool owner) [Fact] [SmokeTestFor(typeof(MySessionsPage))] - public void Sessions_render_empty_service_result() + public async Task Sessions_render_empty_service_result() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())) .ReturnsAsync(PageRenderContext.Success(PageRenderContext.Empty())); var page = context.RenderPage(PageRenderContext.Authenticated("User")); @@ -116,9 +116,9 @@ public void Sessions_render_empty_service_result() } [Fact] - public void Sessions_render_loading_then_explicit_service_failure() + public async Task Sessions_render_loading_then_explicit_service_failure() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); var response = new TaskCompletionSource>>( TaskCreationOptions.RunContinuationsAsynchronously); context.Sessions.Setup(x => x.GetMyChainsAsync(It.IsAny())).Returns(response.Task); @@ -137,9 +137,9 @@ public void Sessions_render_loading_then_explicit_service_failure() [InlineData(false, "No roles available.")] [InlineData(true, "Authorization service is unavailable.")] [SmokeTestFor(typeof(AuthorizationPage))] - public void Authorization_management_renders_empty_and_failure_states(bool failure, string expected) + public async Task Authorization_management_renders_empty_and_failure_states(bool failure, string expected) { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); var query = context.Authorization.Setup(x => x.QueryRolesAsync(It.IsAny())); if (failure) query.ThrowsAsync(new HttpRequestException("Simulated unavailable service")); diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs index 499c2e3c..fab7ee53 100644 --- a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/Rendering/RemainingPageRenderTests.cs @@ -15,7 +15,7 @@ public sealed class RemainingPageRenderTests [Fact, SmokeTestFor(typeof(UsersPage))] public async Task Owner_can_render_sandbox_users() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddManagementServices(); await context.SeedSandboxAsync(owner: true); var page = AssertPage(context, "Users", "Owner"); @@ -25,7 +25,7 @@ public async Task Owner_can_render_sandbox_users() [Fact, SmokeTestFor(typeof(MyIdentityPage))] public async Task User_can_render_profile_identifiers_and_credentials() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddManagementServices(); context.SetupProfileAndAccess(); await context.SeedSandboxAsync(owner: false); @@ -40,7 +40,7 @@ public async Task User_can_render_profile_identifiers_and_credentials() [Fact, SmokeTestFor(typeof(MyAccessPage))] public async Task User_can_render_access_with_no_assigned_roles_or_permissions() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddManagementServices(); context.SetupProfileAndAccess(); await context.SeedSandboxAsync(owner: false); @@ -56,7 +56,7 @@ public async Task User_can_render_access_with_no_assigned_roles_or_permissions() [Fact, SmokeTestFor(typeof(SettingsPage))] public async Task Owner_can_render_sandbox_settings() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddManagementServices(); await context.SeedSandboxAsync(owner: true); var page = AssertPage(context, "Sandbox Settings", "Owner"); @@ -66,7 +66,7 @@ public async Task Owner_can_render_sandbox_settings() [Fact, SmokeTestFor(typeof(ActivityPage))] public async Task User_can_render_empty_sandbox_activity() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddLabServices(); await context.SeedSandboxAsync(owner: false); var page = AssertPage(context, "Activity", "User"); @@ -74,9 +74,9 @@ public async Task User_can_render_empty_sandbox_activity() } [Fact, SmokeTestFor(typeof(AdminPage))] - public void Ladmin_can_render_administration_and_repair_panel() + public async Task Ladmin_can_render_administration_and_repair_panel() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddAdminServices(); var page = AssertPage(context, "Sandbox Management", "Ladmin"); page.WaitForAssertion(() => @@ -98,7 +98,7 @@ public async Task User_can_render_current_session_diagnostics() [Fact, SmokeTestFor(typeof(DemoStorePage))] public async Task User_can_render_store_without_demo_role_grants() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); context.AddDemoStoreServices(); await context.SeedSandboxAsync(owner: false); var page = AssertPage(context, "Demo Store", "User"); @@ -106,77 +106,76 @@ public async Task User_can_render_store_without_demo_role_grants() } [Fact, SmokeTestFor(typeof(AuthorizationLabPage))] - public void User_can_render_authorization_lab_index() + public async Task User_can_render_authorization_lab_index() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Authorization Lab", "User"); } [Fact, SmokeTestFor(typeof(Error))] - public void Error_page_renders_without_a_request_context() + public async Task Error_page_renders_without_a_request_context() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Error."); } [Fact, SmokeTestFor(typeof(PublicPage))] - public void Public_authorization_sample_renders() + public async Task Public_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Public Access"); } [Fact, SmokeTestFor(typeof(AuthenticatedPage))] - public void Authenticated_authorization_sample_renders() + public async Task Authenticated_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Authenticated Page", "User"); } [Fact, SmokeTestFor(typeof(AdminRolePage))] - public void Admin_role_authorization_sample_renders() + public async Task Admin_role_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Admin Role", "Admin"); } [Fact, SmokeTestFor(typeof(AdminPermissionPage))] - public void Admin_permission_authorization_sample_renders() + public async Task Admin_permission_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Admin Permission", "Admin", adminPermission: true); } [Fact, SmokeTestFor(typeof(AspNetAuthorizePage))] - public void Aspnet_authorization_sample_renders() + public async Task Aspnet_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "ASP.NET Core Authorize", "User"); } [Fact, SmokeTestFor(typeof(CombinedAuthorizationPage))] - public void Combined_authorization_sample_renders() + public async Task Combined_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Combined Authorization", "Admin", adminPermission: true); } [Fact, SmokeTestFor(typeof(StateViewPage))] - public void State_view_authorization_sample_renders() + public async Task State_view_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "UAuthStateView", "Admin", authorizationExamples: true); } [Fact, SmokeTestFor(typeof(MatchModesPage))] - public void Match_modes_authorization_sample_renders() + public async Task Match_modes_authorization_sample_renders() { - using var context = new PageRenderContext(); + await using var context = new PageRenderContext(); AssertPage(context, "Authorization Match Modes", "Admin", authorizationExamples: true); } - private static IRenderedComponent AssertPage(PageRenderContext context, string expected, string? role = null, bool authorizationExamples = false, bool adminPermission = false) - where T : IComponent + private static IRenderedComponent AssertPage(PageRenderContext context, string expected, string? role = null, bool authorizationExamples = false, bool adminPermission = false) where T : IComponent { var page = context.RenderPage(role is null ? null : PageRenderContext.Authenticated(role, adminPermission ? new[] { UAuthActions.Users.QueryAdmin } : Array.Empty())); page.WaitForAssertion(() => @@ -185,7 +184,7 @@ private static IRenderedComponent AssertPage(PageRenderContext context, st // A heading must not mask initialization falling into an error fallback. // These two sample pages deliberately show denied authorization examples. if (!authorizationExamples) - Assert.DoesNotContain(page.FindComponents(), alert => alert.Instance.Severity == Severity.Error); + Assert.Empty(page.FindAll(".mud-alert-error")); }); return page; } From 0519926de2231a950bc1ea7d6d16a3f0444e3884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Can=20Karag=C3=B6z?= Date: Sun, 1 Nov 2026 00:38:11 +0300 Subject: [PATCH 3/3] Add Test Runsettings --- .github/workflows/ultimateauth-ci.yml | 3 ++- .../identity-lab.runsettings | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings diff --git a/.github/workflows/ultimateauth-ci.yml b/.github/workflows/ultimateauth-ci.yml index 6058261f..0b38ddf7 100644 --- a/.github/workflows/ultimateauth-ci.yml +++ b/.github/workflows/ultimateauth-ci.yml @@ -55,7 +55,7 @@ jobs: identity-lab-security: - name: Identity Lab security (PostgreSQL) + name: Identity Lab security runs-on: ubuntu-latest timeout-minutes: 20 @@ -75,6 +75,7 @@ jobs: --collect:"XPlat Code Coverage" --logger "trx;LogFileName=identity-lab.trx" --results-directory TestResults/IdentityLab + --settings tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings - name: Generate Identity Lab coverage summary if: always() diff --git a/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings new file mode 100644 index 00000000..87dfcfab --- /dev/null +++ b/tests/CodeBeam.UltimateAuth.IdentityLab.Tests/identity-lab.runsettings @@ -0,0 +1,14 @@ + + + + + + + [CodeBeam.UltimateAuth.IdentityLab]* + **/Migrations/**/*.cs + cobertura,json + + + + +