diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor index c5ee6a53e20a..1c360b7121a7 100644 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor +++ b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/App.razor @@ -1,5 +1,10 @@ - + + + + -

Sorry, there's nothing at this address.

+ +

Sorry, there's nothing at this address.

+
diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor deleted file mode 100644 index 0f24edaf1dd2..000000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor index 7f4cf93fbb61..fe885300e78b 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor +++ b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor @@ -1 +1,8 @@ - + + + + + + Sorry, there's nothing here. + + diff --git a/src/Components/Blazor/testassets/StandaloneApp/App.razor b/src/Components/Blazor/testassets/StandaloneApp/App.razor index 4b8a0ffa42de..fe6830bb0156 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/App.razor +++ b/src/Components/Blazor/testassets/StandaloneApp/App.razor @@ -1,5 +1,11 @@ - - + + + + + + +

Not found

+ Sorry, there's nothing at this address. +
+
+
diff --git a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor b/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor deleted file mode 100644 index 5e11c2a20cec..000000000000 --- a/src/Components/Blazor/testassets/StandaloneApp/Pages/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@layout MainLayout diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 4f317be0ff44..98b2644ab8bd 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -16,6 +16,15 @@ public event Microsoft.AspNetCore.Components.AuthenticationStateChangedHandler A public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task task) { } } + public sealed partial class AuthorizeRouteView : Microsoft.AspNetCore.Components.RouteView + { + public AuthorizeRouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected override void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + } public partial class AuthorizeView : Microsoft.AspNetCore.Components.AuthorizeViewCore { public AuthorizeView() { } @@ -278,6 +287,16 @@ protected LayoutComponentBase() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment Body { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } } + public partial class LayoutView : Microsoft.AspNetCore.Components.IComponent + { + public LayoutView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type Layout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } public sealed partial class LocationChangeException : System.Exception { public LocationChangeException(string message, System.Exception innerException) { } @@ -323,20 +342,6 @@ public abstract partial class OwningComponentBase : Microsoft.AspNetCo protected OwningComponentBase() { } protected TService Service { get { throw null; } } } - public partial class PageDisplay : Microsoft.AspNetCore.Components.IComponent - { - public PageDisplay() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Type Page { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public System.Collections.Generic.IDictionary PageParameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } - public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } - } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] public sealed partial class ParameterAttribute : System.Attribute { @@ -391,6 +396,23 @@ public sealed partial class RouteAttribute : System.Attribute public RouteAttribute(string template) { } public string Template { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } + public sealed partial class RouteData + { + public RouteData(System.Type pageType, System.Collections.Generic.IReadOnlyDictionary routeValues) { } + public System.Type PageType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Collections.Generic.IReadOnlyDictionary RouteValues { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + public partial class RouteView : Microsoft.AspNetCore.Components.IComponent + { + public RouteView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Type DefaultLayout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RouteData RouteData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } + protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } + } } namespace Microsoft.AspNetCore.Components.CompilerServices { @@ -648,15 +670,12 @@ public Router() { } [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public Microsoft.AspNetCore.Components.RenderFragment Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment NotFound { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { } public void Dispose() { } System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; } - protected virtual void Render(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder, System.Type handler, System.Collections.Generic.IDictionary parameters) { } public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; } } } diff --git a/src/Components/Components/src/Auth/AuthorizeRouteView.cs b/src/Components/Components/src/Auth/AuthorizeRouteView.cs new file mode 100644 index 000000000000..164d752c3ff0 --- /dev/null +++ b/src/Components/Components/src/Auth/AuthorizeRouteView.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.Auth; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Combines the behaviors of and , + /// so that it displays the page matching the specified route but only if the user + /// is authorized to see it. + /// + /// Additionally, this component supplies a cascading parameter of type , + /// which makes the user's current authentication state available to descendants. + /// + public sealed class AuthorizeRouteView : RouteView + { + // We expect applications to supply their own authorizing/not-authorized content, but + // it's better to have defaults than to make the parameters mandatory because in some + // cases they will never be used (e.g., "authorizing" in out-of-box server-side Blazor) + private static readonly RenderFragment _defaultNotAuthorizedContent + = state => builder => builder.AddContent(0, "Not authorized"); + private static readonly RenderFragment _defaultAuthorizingContent + = builder => builder.AddContent(0, "Authorizing..."); + + private readonly RenderFragment _renderAuthorizeRouteViewCoreDelegate; + private readonly RenderFragment _renderAuthorizedDelegate; + private readonly RenderFragment _renderNotAuthorizedDelegate; + private readonly RenderFragment _renderAuthorizingDelegate; + + public AuthorizeRouteView() + { + // Cache the rendering delegates so that we only construct new closure instances + // when they are actually used (e.g., we never prepare a RenderFragment bound to + // the NotAuthorized content except when you are displaying that particular state) + RenderFragment renderBaseRouteViewDelegate = builder => base.Render(builder); + _renderAuthorizedDelegate = authenticateState => renderBaseRouteViewDelegate; + _renderNotAuthorizedDelegate = authenticationState => builder => RenderNotAuthorizedInDefaultLayout(builder, authenticationState); + _renderAuthorizingDelegate = RenderAuthorizingInDefaultLayout; + _renderAuthorizeRouteViewCoreDelegate = RenderAuthorizeRouteViewCore; + } + + /// + /// The content that will be displayed if the user is not authorized. + /// + [Parameter] + public RenderFragment NotAuthorized { get; set; } + + /// + /// The content that will be displayed while asynchronous authorization is in progress. + /// + [Parameter] + public RenderFragment Authorizing { get; set; } + + [CascadingParameter] + private Task ExistingCascadedAuthenticationState { get; set; } + + /// + protected override void Render(RenderTreeBuilder builder) + { + if (ExistingCascadedAuthenticationState != null) + { + // If this component is already wrapped in a (or another + // compatible provider), then don't interfere with the cascaded authentication state. + _renderAuthorizeRouteViewCoreDelegate(builder); + } + else + { + // Otherwise, implicitly wrap the output in a + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(CascadingAuthenticationState.ChildContent), _renderAuthorizeRouteViewCoreDelegate); + builder.CloseComponent(); + } + } + + private void RenderAuthorizeRouteViewCore(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteViewCore.RouteData), RouteData); + builder.AddAttribute(2, nameof(AuthorizeRouteViewCore.Authorized), _renderAuthorizedDelegate); + builder.AddAttribute(3, nameof(AuthorizeRouteViewCore.Authorizing), _renderAuthorizingDelegate); + builder.AddAttribute(4, nameof(AuthorizeRouteViewCore.NotAuthorized), _renderNotAuthorizedDelegate); + builder.CloseComponent(); + } + + private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content) + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), DefaultLayout); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), content); + builder.CloseComponent(); + } + + private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState) + { + var content = NotAuthorized ?? _defaultNotAuthorizedContent; + RenderContentInDefaultLayout(builder, content(authenticationState)); + } + + private void RenderAuthorizingInDefaultLayout(RenderTreeBuilder builder) + { + var content = Authorizing ?? _defaultAuthorizingContent; + RenderContentInDefaultLayout(builder, content); + } + + private class AuthorizeRouteViewCore : AuthorizeViewCore + { + [Parameter] + public RouteData RouteData { get; set; } + + protected override IAuthorizeData[] GetAuthorizeData() + => AttributeAuthorizeDataCache.GetAuthorizeDataForType(RouteData.PageType); + } + } +} diff --git a/src/Components/Components/src/Auth/AuthorizeViewCore.cs b/src/Components/Components/src/Auth/AuthorizeViewCore.cs index 8c60078184f8..1a6cc9a09b20 100644 --- a/src/Components/Components/src/Auth/AuthorizeViewCore.cs +++ b/src/Components/Components/src/Auth/AuthorizeViewCore.cs @@ -52,6 +52,8 @@ public abstract class AuthorizeViewCore : ComponentBase /// protected override void BuildRenderTree(RenderTreeBuilder builder) { + // We're using the same sequence number for each of the content items here + // so that we can update existing instances if they are the same shape if (currentAuthenticationState == null) { builder.AddContent(0, Authorizing); @@ -59,11 +61,11 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) else if (isAuthorized) { var authorized = Authorized ?? ChildContent; - builder.AddContent(1, authorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, authorized?.Invoke(currentAuthenticationState)); } else { - builder.AddContent(2, NotAuthorized?.Invoke(currentAuthenticationState)); + builder.AddContent(0, NotAuthorized?.Invoke(currentAuthenticationState)); } } @@ -102,6 +104,12 @@ protected override async Task OnParametersSetAsync() private async Task IsAuthorizedAsync(ClaimsPrincipal user) { var authorizeData = GetAuthorizeData(); + if (authorizeData == null) + { + // No authorization applies, so no need to consult the authorization service + return true; + } + EnsureNoAuthenticationSchemeSpecified(authorizeData); var policy = await AuthorizationPolicy.CombineAsync( diff --git a/src/Components/Components/src/LayoutView.cs b/src/Components/Components/src/LayoutView.cs new file mode 100644 index 000000000000..7b88d181f0e0 --- /dev/null +++ b/src/Components/Components/src/LayoutView.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified content inside the specified layout and any further + /// nested layouts. + /// + public class LayoutView : IComponent + { + private static readonly RenderFragment EmptyRenderFragment = builder => { }; + + private RenderHandle _renderHandle; + + /// + /// Gets or sets the content to display. + /// + [Parameter] + public RenderFragment ChildContent { get; set; } + + /// + /// Gets or sets the type of the layout in which to display the content. + /// The type must implement and accept a parameter named . + /// + [Parameter] + public Type Layout { get; set; } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + Render(); + return Task.CompletedTask; + } + + private void Render() + { + // In the middle goes the supplied content + var fragment = ChildContent ?? EmptyRenderFragment; + + // Then repeatedly wrap that in each layer of nested layout until we get + // to a layout that has no parent + var layoutType = Layout; + while (layoutType != null) + { + fragment = WrapInLayout(layoutType, fragment); + layoutType = GetParentLayoutType(layoutType); + } + + _renderHandle.Render(fragment); + } + + private static RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) + { + return builder => + { + builder.OpenComponent(0, layoutType); + builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); + builder.CloseComponent(); + }; + } + + private static Type GetParentLayoutType(Type type) + => type.GetCustomAttribute()?.LayoutType; + } +} diff --git a/src/Components/Components/src/PageDisplay.cs b/src/Components/Components/src/PageDisplay.cs deleted file mode 100644 index 14df824d1393..000000000000 --- a/src/Components/Components/src/PageDisplay.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Components.Auth; -using Microsoft.AspNetCore.Components.RenderTree; - -namespace Microsoft.AspNetCore.Components -{ - /// - /// Displays the specified page component, rendering it inside its layout - /// and any further nested layouts, plus applying any authorization rules. - /// - public class PageDisplay : IComponent - { - private RenderHandle _renderHandle; - - /// - /// Gets or sets the type of the page component to display. - /// The type must implement . - /// - [Parameter] - public Type Page { get; set; } - - /// - /// Gets or sets the parameters to pass to the page. - /// - [Parameter] - public IDictionary PageParameters { get; set; } - - /// - /// The content that will be displayed if the user is not authorized. - /// - [Parameter] - public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] - public RenderFragment Authorizing { get; set; } - - /// - public void Attach(RenderHandle renderHandle) - { - _renderHandle = renderHandle; - } - - /// - public Task SetParametersAsync(ParameterView parameters) - { - parameters.SetParameterProperties(this); - Render(); - return Task.CompletedTask; - } - - private void Render() - { - // In the middle goes the requested page - var fragment = (RenderFragment)RenderPageWithParameters; - - // Around that goes an AuthorizeViewCore - fragment = WrapInAuthorizeViewCore(fragment); - - // Then repeatedly wrap that in each layer of nested layout until we get - // to a layout that has no parent - Type layoutType = Page; - while ((layoutType = GetLayoutType(layoutType)) != null) - { - fragment = WrapInLayout(layoutType, fragment); - } - - _renderHandle.Render(fragment); - } - - private RenderFragment WrapInLayout(Type layoutType, RenderFragment bodyParam) => builder => - { - builder.OpenComponent(0, layoutType); - builder.AddAttribute(1, LayoutComponentBase.BodyPropertyName, bodyParam); - builder.CloseComponent(); - }; - - private void RenderPageWithParameters(RenderTreeBuilder builder) - { - builder.OpenComponent(0, Page); - - if (PageParameters != null) - { - foreach (var kvp in PageParameters) - { - builder.AddAttribute(1, kvp.Key, kvp.Value); - } - } - - builder.CloseComponent(); - } - - private RenderFragment WrapInAuthorizeViewCore(RenderFragment pageFragment) - { - var authorizeData = AttributeAuthorizeDataCache.GetAuthorizeDataForType(Page); - if (authorizeData == null) - { - // No authorization, so no need to wrap the fragment - return pageFragment; - } - - // Some authorization data exists, so we do need to wrap the fragment - RenderFragment authorized = context => pageFragment; - return builder => - { - builder.OpenComponent(0); - builder.AddAttribute(1, nameof(AuthorizeViewWithSuppliedData.AuthorizeDataParam), authorizeData); - builder.AddAttribute(2, nameof(AuthorizeViewWithSuppliedData.Authorized), authorized); - builder.AddAttribute(3, nameof(AuthorizeViewWithSuppliedData.NotAuthorized), NotAuthorized ?? DefaultNotAuthorized); - builder.AddAttribute(4, nameof(AuthorizeViewWithSuppliedData.Authorizing), Authorizing); - builder.CloseComponent(); - }; - } - - private static Type GetLayoutType(Type type) - => type.GetCustomAttribute()?.LayoutType; - - private class AuthorizeViewWithSuppliedData : AuthorizeViewCore - { - [Parameter] public IAuthorizeData[] AuthorizeDataParam { get; private set; } - - protected override IAuthorizeData[] GetAuthorizeData() => AuthorizeDataParam; - } - - // There has to be some default content. If we render blank by default, developers - // will find it hard to guess why their UI isn't appearing. - private static RenderFragment DefaultNotAuthorized(AuthenticationState authenticationState) - => builder => builder.AddContent(0, "Not authorized"); - } -} diff --git a/src/Components/Components/src/RouteView.cs b/src/Components/Components/src/RouteView.cs new file mode 100644 index 000000000000..5aacff2715d8 --- /dev/null +++ b/src/Components/Components/src/RouteView.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.RenderTree; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Displays the specified page component, rendering it inside its layout + /// and any further nested layouts. + /// + public class RouteView : IComponent + { + private readonly RenderFragment _renderDelegate; + private readonly RenderFragment _renderPageWithParametersDelegate; + private RenderHandle _renderHandle; + + /// + /// Gets or sets the route data. This determines the page that will be + /// displayed and the parameter values that will be supplied to the page. + /// + [Parameter] + public RouteData RouteData { get; set; } + + /// + /// Gets or sets the type of a layout to be used if the page does not + /// declare any layout. If specified, the type must implement + /// and accept a parameter named . + /// + [Parameter] + public Type DefaultLayout { get; set; } + + public RouteView() + { + // Cache the delegate instances + _renderDelegate = Render; + _renderPageWithParametersDelegate = RenderPageWithParameters; + } + + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + if (RouteData == null) + { + throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}."); + } + + _renderHandle.Render(_renderDelegate); + return Task.CompletedTask; + } + + /// + /// Renders the component. + /// + /// The . + protected virtual void Render(RenderTreeBuilder builder) + { + var pageLayoutType = RouteData.PageType.GetCustomAttribute()?.LayoutType + ?? DefaultLayout; + + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(LayoutView.Layout), pageLayoutType); + builder.AddAttribute(2, nameof(LayoutView.ChildContent), _renderPageWithParametersDelegate); + builder.CloseComponent(); + } + + private void RenderPageWithParameters(RenderTreeBuilder builder) + { + builder.OpenComponent(0, RouteData.PageType); + + foreach (var kvp in RouteData.RouteValues) + { + builder.AddAttribute(1, kvp.Key, kvp.Value); + } + + builder.CloseComponent(); + } + } +} diff --git a/src/Components/Components/src/Routing/RouteContext.cs b/src/Components/Components/src/Routing/RouteContext.cs index 7de5f3c61552..7061e9be4105 100644 --- a/src/Components/Components/src/Routing/RouteContext.cs +++ b/src/Components/Components/src/Routing/RouteContext.cs @@ -26,6 +26,6 @@ public RouteContext(string path) public Type Handler { get; set; } - public IDictionary Parameters { get; set; } + public IReadOnlyDictionary Parameters { get; set; } } } diff --git a/src/Components/Components/src/Routing/RouteData.cs b/src/Components/Components/src/Routing/RouteData.cs new file mode 100644 index 000000000000..e0da00f0c747 --- /dev/null +++ b/src/Components/Components/src/Routing/RouteData.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// Describes information determined during routing that specifies + /// the page to be displayed. + /// + public sealed class RouteData + { + /// + /// Constructs an instance of . + /// + /// The type of the page matching the route, which must implement . + /// The route parameter values extracted from the matched route. + public RouteData(Type pageType, IReadOnlyDictionary routeValues) + { + if (pageType == null) + { + throw new ArgumentNullException(nameof(pageType)); + } + + if (!typeof(IComponent).IsAssignableFrom(pageType)) + { + throw new ArgumentException($"The value must implement {nameof(IComponent)}.", nameof(pageType)); + } + + PageType = pageType; + RouteValues = routeValues ?? throw new ArgumentNullException(nameof(routeValues)); + } + + /// + /// Gets the type of the page matching the route. + /// + public Type PageType { get; } + + /// + /// Gets route parameter values extracted from the matched route. + /// + public IReadOnlyDictionary RouteValues { get; } + } +} diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 96eca173227c..509975a75479 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,20 +3,21 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Routing { /// - /// A component that displays whichever other component corresponds to the - /// current navigation location. + /// A component that supplies route data corresponding to the current navigation state. /// public class Router : IComponent, IHandleAfterRender, IDisposable { static readonly char[] _queryOrHashStartChar = new[] { '?', '#' }; + static readonly ReadOnlyDictionary _emptyParametersDictionary + = new ReadOnlyDictionary(new Dictionary()); RenderHandle _renderHandle; string _baseUri; @@ -33,25 +34,19 @@ public class Router : IComponent, IHandleAfterRender, IDisposable [Inject] private ILoggerFactory LoggerFactory { get; set; } /// - /// Gets or sets the assembly that should be searched, along with its referenced - /// assemblies, for components matching the URI. + /// Gets or sets the assembly that should be searched for components matching the URI. /// [Parameter] public Assembly AppAssembly { get; set; } /// - /// Gets or sets the type of the component that should be used as a fallback when no match is found for the requested route. + /// Gets or sets the content to display when no match is found for the requested route. /// [Parameter] public RenderFragment NotFound { get; set; } /// - /// The content that will be displayed if the user is not authorized. + /// Gets or sets the content to display when a match is found for the requested route. /// - [Parameter] public RenderFragment NotAuthorized { get; set; } - - /// - /// The content that will be displayed while asynchronous authorization is in progress. - /// - [Parameter] public RenderFragment Authorizing { get; set; } + [Parameter] public RenderFragment Found { get; set; } private RouteTable Routes { get; set; } @@ -69,6 +64,22 @@ public void Attach(RenderHandle renderHandle) public Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); + + // Found content is mandatory, because even though we could use something like as a + // reasonable default, if it's not declared explicitly in the template then people will have no way + // to discover how to customize this (e.g., to add authorization). + if (Found == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}."); + } + + // NotFound content is mandatory, because even though we could display a default message like "Not found", + // it has to be specified explicitly so that it can also be wrapped in a specific layout + if (NotFound == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}."); + } + Routes = RouteTableFactory.Create(AppAssembly); Refresh(isNavigationIntercepted: false); return Task.CompletedTask; @@ -80,7 +91,7 @@ public void Dispose() NavigationManager.LocationChanged -= OnLocationChanged; } - private string StringUntilAny(string str, char[] chars) + private static string StringUntilAny(string str, char[] chars) { var firstIndex = str.IndexOfAny(chars); return firstIndex < 0 @@ -88,17 +99,6 @@ private string StringUntilAny(string str, char[] chars) : str.Substring(0, firstIndex); } - /// - protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary parameters) - { - builder.OpenComponent(0, typeof(PageDisplay)); - builder.AddAttribute(1, nameof(PageDisplay.Page), handler); - builder.AddAttribute(2, nameof(PageDisplay.PageParameters), parameters); - builder.AddAttribute(3, nameof(PageDisplay.NotAuthorized), NotAuthorized); - builder.AddAttribute(4, nameof(PageDisplay.Authorizing), Authorizing); - builder.CloseComponent(); - } - private void Refresh(bool isNavigationIntercepted) { var locationPath = NavigationManager.ToBaseRelativePath(_locationAbsolute); @@ -116,16 +116,19 @@ private void Refresh(bool isNavigationIntercepted) Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); - _renderHandle.Render(builder => Render(builder, context.Handler, context.Parameters)); + var routeData = new RouteData( + context.Handler, + context.Parameters ?? _emptyParametersDictionary); + _renderHandle.Render(Found(routeData)); } else { - if (!isNavigationIntercepted && NotFound != null) + if (!isNavigationIntercepted) { Log.DisplayingNotFound(_logger, locationPath, _baseUri); // We did not find a Component that matches the route. - // Only show the NotFound if the application developer programatically got us here i.e we did not + // Only show the NotFound content if the application developer programatically got us here i.e we did not // intercept the navigation. In all other cases, force a browser navigation since this could be non-Blazor content. _renderHandle.Render(NotFound); } diff --git a/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs new file mode 100644 index 000000000000..36add49fa335 --- /dev/null +++ b/src/Components/Components/test/Auth/AuthorizeRouteViewTest.cs @@ -0,0 +1,355 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Components +{ + public class AuthorizeRouteViewTest + { + private readonly static IReadOnlyDictionary EmptyParametersDictionary = new Dictionary(); + private readonly TestAuthenticationStateProvider _authenticationStateProvider; + private readonly TestRenderer _renderer; + private readonly RouteView _authorizeRouteViewComponent; + private readonly int _authorizeRouteViewComponentId; + private readonly TestAuthorizationService _testAuthorizationService; + + public AuthorizeRouteViewTest() + { + _authenticationStateProvider = new TestAuthenticationStateProvider(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult( + new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); + + _testAuthorizationService = new TestAuthorizationService(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(_authenticationStateProvider); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(_testAuthorizationService); + + _renderer = new TestRenderer(serviceCollection.BuildServiceProvider()); + _authorizeRouteViewComponent = new AuthorizeRouteView(); + _authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent); + } + + [Fact] + public void WhenAuthorized_RendersPageInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), new Dictionary + { + { nameof(TestPageRequiringAuthorization.Message), "Hello, world!" } + }); + _testAuthorizationService.NextResult = AuthorizationResult.Success(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + AssertFrame.Component(batch.ReferenceFrames[edit.ReferenceFrameIndex]); + }, + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Assert: renders page + var pageDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(pageDiff.Edits, + edit => AssertPrependText(batch, edit, "Hello from the page with message: Hello, world!")); + } + + [Fact] + public void WhenNotAuthorized_RendersDefaultNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + + // Act + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Not authorized"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WhenNotAuthorized_RendersCustomNotAuthorizedContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + _testAuthorizationService.NextResult = AuthorizationResult.Failed(); + _authenticationStateProvider.CurrentAuthStateTask = Task.FromResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + + // Act + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "not authorized" message + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Go away, Bert"), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public async Task WhenAuthorizing_RendersDefaultAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customNotAuthorized = + state => builder => builder.AddContent(0, $"Go away, {state.User.Identity.Name}"); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.NotAuthorized), customNotAuthorized }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Authorizing..."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + + // Act 2: updates when authorization completes + authStateTcs.SetResult(new AuthenticationState( + new ClaimsPrincipal(new TestIdentity { Name = "Bert" }))); + await firstRenderTask; + + // Assert 2: Only the layout is updated + batch = _renderer.Batches.Skip(1).Single(); + var nonEmptyDiff = batch.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Equal(layoutDiff.ComponentId, nonEmptyDiff.ComponentId); + Assert.Collection(nonEmptyDiff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch.ReferenceFrames[edit.ReferenceFrameIndex], "Go away, Bert"); + }); + } + + [Fact] + public void WhenAuthorizing_RendersCustomAuthorizingContentInsideLayout() + { + // Arrange + var routeData = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var authStateTcs = new TaskCompletionSource(); + _authenticationStateProvider.CurrentAuthStateTask = authStateTcs.Task; + RenderFragment customAuthorizing = + builder => builder.AddContent(0, "Hold on, we're checking your papers."); + + // Act + var firstRenderTask = _renderer.RenderRootComponentAsync(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + { nameof(AuthorizeRouteView.Authorizing), customAuthorizing }, + })); + + // Assert: renders layout containing "authorizing" message + Assert.False(firstRenderTask.IsCompleted); + var batch = _renderer.Batches.Single(); + var layoutDiff = batch.GetComponentDiffs().Single(); + Assert.Collection(layoutDiff.Edits, + edit => AssertPrependText(batch, edit, "Layout starts here"), + edit => AssertPrependText(batch, edit, "Hold on, we're checking your papers."), + edit => AssertPrependText(batch, edit, "Layout ends here")); + } + + [Fact] + public void WithoutCascadedAuthenticationState_WrapsOutputInCascadingAuthenticationState() + { + // Arrange/Act + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData } + })); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the hierarchy inside the AuthorizeRouteView, which contains its + // own CascadingAuthenticationState + component => Assert.IsType(component), + component => Assert.IsType>>(component), + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void WithCascadedAuthenticationState_DoesNotWrapOutputInCascadingAuthenticationState() + { + // Arrange + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + var rootComponent = new AuthorizeRouteViewWithExistingCascadedAuthenticationState( + _authenticationStateProvider.CurrentAuthStateTask, + routeData); + var rootComponentId = _renderer.AssignRootComponentId(rootComponent); + + // Act + _renderer.RenderRootComponent(rootComponentId); + + // Assert + var batch = _renderer.Batches.Single(); + var componentInstances = batch.ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Select(f => f.Component); + + Assert.Collection(componentInstances, + // This is the externally-supplied cascading value + component => Assert.IsType>>(component), + component => Assert.IsType(component), + + // This is the hierarchy inside the AuthorizeRouteView. It doesn't contain a + // further CascadingAuthenticationState + component => Assert.IsAssignableFrom(component), + component => Assert.IsType(component), + component => Assert.IsType(component)); + } + + [Fact] + public void UpdatesOutputWhenRouteDataChanges() + { + // Arrange/Act 1: Start on some route + // Not asserting about the initial output, as that is covered by other tests + var routeData = new RouteData(typeof(TestPageWithNoAuthorization), EmptyParametersDictionary); + _renderer.RenderRootComponent(_authorizeRouteViewComponentId, ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData }, + { nameof(AuthorizeRouteView.DefaultLayout), typeof(TestLayout) }, + })); + + // Act 2: Move to another route + var routeData2 = new RouteData(typeof(TestPageRequiringAuthorization), EmptyParametersDictionary); + var render2Task = _renderer.Dispatcher.InvokeAsync(() => _authorizeRouteViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(AuthorizeRouteView.RouteData), routeData2 }, + }))); + + // Assert: we retain the layout instance, and mutate its contents + Assert.True(render2Task.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch2 = _renderer.Batches[1]; + var diff = batch2.DiffsInOrder.Where(d => d.Edits.Any()).Single(); + Assert.Collection(diff.Edits, + edit => + { + // Inside the layout, we add the new content + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text(batch2.ReferenceFrames[edit.ReferenceFrameIndex], "Not authorized"); + }, + edit => + { + // ... and remove the old content + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + }); + } + + private static void AssertPrependText(CapturedBatch batch, RenderTreeEdit edit, string text) + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + ref var referenceFrame = ref batch.ReferenceFrames[edit.ReferenceFrameIndex]; + AssertFrame.Text(referenceFrame, text); + } + + class TestPageWithNoAuthorization : ComponentBase { } + + [Authorize] + class TestPageRequiringAuthorization : ComponentBase + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message: {Message}"); + } + } + + class TestLayout : LayoutComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + class AuthorizeRouteViewWithExistingCascadedAuthenticationState : AutoRenderComponent + { + private readonly Task _authenticationState; + private readonly RouteData _routeData; + + public AuthorizeRouteViewWithExistingCascadedAuthenticationState( + Task authenticationState, + RouteData routeData) + { + _authenticationState = authenticationState; + _routeData = routeData; + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent>>(0); + builder.AddAttribute(1, nameof(CascadingValue.Value), _authenticationState); + builder.AddAttribute(2, nameof(CascadingValue.ChildContent), (RenderFragment)(builder => + { + builder.OpenComponent(0); + builder.AddAttribute(1, nameof(AuthorizeRouteView.RouteData), _routeData); + builder.CloseComponent(); + })); + builder.CloseComponent(); + } + } + } +} diff --git a/src/Components/Components/test/Auth/AuthorizeViewTest.cs b/src/Components/Components/test/Auth/AuthorizeViewTest.cs index 848e68e815e1..0c890ee0c12f 100644 --- a/src/Components/Components/test/Auth/AuthorizeViewTest.cs +++ b/src/Components/Components/test/Auth/AuthorizeViewTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Claims; @@ -331,15 +330,9 @@ public void RendersAuthorizingUntilAuthorizationCompleted() Assert.Equal(2, renderer.Batches.Count); var batch2 = renderer.Batches[1]; var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single(); - Assert.Collection(diff2.Edits, - edit => - { - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(0, edit.SiblingIndex); - }, - edit => + Assert.Collection(diff2.Edits, edit => { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); Assert.Equal(0, edit.SiblingIndex); AssertFrame.Text( batch2.ReferenceFrames[edit.ReferenceFrameIndex], @@ -513,15 +506,6 @@ public static Task CreateAuthenticationState(string usernam => Task.FromResult(new AuthenticationState( new ClaimsPrincipal(new TestIdentity { Name = username }))); - class TestIdentity : IIdentity - { - public string AuthenticationType => "Test"; - - public bool IsAuthenticated => true; - - public string Name { get; set; } - } - public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService) { var serviceCollection = new ServiceCollection(); @@ -530,52 +514,6 @@ public TestRenderer CreateTestRenderer(IAuthorizationService authorizationServic return new TestRenderer(serviceCollection.BuildServiceProvider()); } - private class TestAuthorizationService : IAuthorizationService - { - public AuthorizationResult NextResult { get; set; } - = AuthorizationResult.Failed(); - - public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } - = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) - { - AuthorizeCalls.Add((user, resource, requirements)); - - // The TestAuthorizationService doesn't actually apply any authorization requirements - // It just returns the specified NextResult, since we're not trying to test the logic - // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired - // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. - return Task.FromResult(NextResult); - } - - public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) - => throw new NotImplementedException(); - } - - private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider - { - private readonly AuthorizationOptions options = new AuthorizationOptions(); - - public Task GetDefaultPolicyAsync() - => Task.FromResult(options.DefaultPolicy); - - public Task GetFallbackPolicyAsync() - => Task.FromResult(options.FallbackPolicy); - - public Task GetPolicyAsync(string policyName) => Task.FromResult( - new AuthorizationPolicy(new[] - { - new TestPolicyRequirement { PolicyName = policyName } - }, - new[] { $"TestScheme:{policyName}" })); - } - - public class TestPolicyRequirement : IAuthorizationRequirement - { - public string PolicyName { get; set; } - } - public class AuthorizeViewCoreWithScheme : AuthorizeViewCore { protected override IAuthorizeData[] GetAuthorizeData() diff --git a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs index 21a269932c67..a543682d4ad3 100644 --- a/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs +++ b/src/Components/Components/test/Auth/CascadingAuthenticationStateTest.cs @@ -37,7 +37,7 @@ public void SuppliesSynchronouslyAvailableAuthStateToChildContent() { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert")) }; @@ -70,7 +70,7 @@ public void SuppliesAsynchronouslyAvailableAuthStateToChildContent() // Arrange: Service var services = new ServiceCollection(); var authStateTaskCompletionSource = new TaskCompletionSource(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = authStateTaskCompletionSource.Task }; @@ -122,7 +122,7 @@ public void RespondsToNotificationsFromAuthenticationStateProvider() { // Arrange: Service var services = new ServiceCollection(); - var authStateProvider = new TestAuthStateProvider() + var authStateProvider = new TestAuthenticationStateProvider() { CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null)) }; @@ -189,21 +189,6 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) } } - class TestAuthStateProvider : AuthenticationStateProvider - { - public Task CurrentAuthStateTask { get; set; } - - public override Task GetAuthenticationStateAsync() - { - return CurrentAuthStateTask; - } - - internal void TriggerAuthenticationStateChanged(Task authState) - { - NotifyAuthenticationStateChanged(authState); - } - } - public static AuthenticationState CreateAuthenticationState(string username) => new AuthenticationState(new ClaimsPrincipal(username == null ? new ClaimsIdentity() diff --git a/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs new file mode 100644 index 000000000000..6a84916e93b0 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthenticationStateProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthenticationStateProvider : AuthenticationStateProvider + { + public Task CurrentAuthStateTask { get; set; } + + public override Task GetAuthenticationStateAsync() + { + return CurrentAuthStateTask; + } + + internal void TriggerAuthenticationStateChanged(Task authState) + { + NotifyAuthenticationStateChanged(authState); + } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs new file mode 100644 index 000000000000..b3e903fdb3f4 --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationPolicyProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider + { + private readonly AuthorizationOptions options = new AuthorizationOptions(); + + public Task GetDefaultPolicyAsync() + => Task.FromResult(options.DefaultPolicy); + + public Task GetFallbackPolicyAsync() + => Task.FromResult(options.FallbackPolicy); + + public Task GetPolicyAsync(string policyName) => Task.FromResult( + new AuthorizationPolicy(new[] + { + new TestPolicyRequirement { PolicyName = policyName } + }, + new[] { $"TestScheme:{policyName}" })); + } + + public class TestPolicyRequirement : IAuthorizationRequirement + { + public string PolicyName { get; set; } + } +} diff --git a/src/Components/Components/test/Auth/TestAuthorizationService.cs b/src/Components/Components/test/Auth/TestAuthorizationService.cs new file mode 100644 index 000000000000..d6cc1ff11a3f --- /dev/null +++ b/src/Components/Components/test/Auth/TestAuthorizationService.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.Components +{ + public class TestAuthorizationService : IAuthorizationService + { + public AuthorizationResult NextResult { get; set; } + = AuthorizationResult.Failed(); + + public List<(ClaimsPrincipal user, object resource, IEnumerable requirements)> AuthorizeCalls { get; } + = new List<(ClaimsPrincipal user, object resource, IEnumerable requirements)>(); + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable requirements) + { + AuthorizeCalls.Add((user, resource, requirements)); + + // The TestAuthorizationService doesn't actually apply any authorization requirements + // It just returns the specified NextResult, since we're not trying to test the logic + // in DefaultAuthorizationService or similar here. So it's up to tests to set a desired + // NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls. + return Task.FromResult(NextResult); + } + + public Task AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName) + => throw new NotImplementedException(); + } +} diff --git a/src/Components/Components/test/Auth/TestIdentity.cs b/src/Components/Components/test/Auth/TestIdentity.cs new file mode 100644 index 000000000000..d650c53fe614 --- /dev/null +++ b/src/Components/Components/test/Auth/TestIdentity.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Principal; + +namespace Microsoft.AspNetCore.Components +{ + public class TestIdentity : IIdentity + { + public string AuthenticationType => "Test"; + + public bool IsAuthenticated => true; + + public string Name { get; set; } + } +} diff --git a/src/Components/Components/test/LayoutViewTest.cs b/src/Components/Components/test/LayoutViewTest.cs new file mode 100644 index 000000000000..6b0abe527d64 --- /dev/null +++ b/src/Components/Components/test/LayoutViewTest.cs @@ -0,0 +1,325 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class LayoutViewTest + { + private readonly TestRenderer _renderer; + private readonly LayoutView _layoutViewComponent; + private readonly int _layoutViewComponentId; + + public LayoutViewTest() + { + _renderer = new TestRenderer(); + _layoutViewComponent = new LayoutView(); + _layoutViewComponentId = _renderer.AssignRootComponentId(_layoutViewComponent); + } + + [Fact] + public void GivenNoParameters_RendersNothing() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.Empty)); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Empty(frames); + } + + [Fact] + public void GivenContentButNoLayout_RendersContent() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + Assert.True(setParametersTask.IsCompletedSuccessfully); + var frames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + + // Assert + Assert.Single(_renderer.Batches); + Assert.Collection(frames, + frame => AssertFrame.Text(frame, "Hello", 123), + frame => AssertFrame.Text(frame, "Goodbye", 456)); + } + + [Fact] + public void GivenLayoutButNoContent_RendersLayoutWithEmptyBody() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) } + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 1), // i.e., empty region + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(RootLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + } + + [Fact] + public void RendersContentInsideNestedLayout() + { + // Arrange/Act + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(123, "Hello"); + builder.AddContent(456, "Goodbye"); + })} + }))); + + // Assert + Assert.True(setParametersTask.IsCompletedSuccessfully); + var batch = _renderer.Batches.Single(); + + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(_layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + var rootLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var rootLayoutFrames = _renderer.GetCurrentRenderTreeFrames(rootLayoutComponentId).AsEnumerable(); + Assert.Collection(rootLayoutFrames, + frame => AssertFrame.Text(frame, "RootLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1), + frame => AssertFrame.Text(frame, "RootLayout ends here", sequence: 2)); + + var nestedLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var nestedLayoutFrames = _renderer.GetCurrentRenderTreeFrames(nestedLayoutComponentId).AsEnumerable(); + Assert.Collection(nestedLayoutFrames, + frame => AssertFrame.Text(frame, "NestedLayout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3, sequence: 1), + frame => AssertFrame.Text(frame, "Hello", sequence: 123), + frame => AssertFrame.Text(frame, "Goodbye", sequence: 456), + frame => AssertFrame.Text(frame, "NestedLayout ends here", sequence: 2)); + } + + [Fact] + public void CanChangeContentWithSameLayout() + { + // Arrange + var setParametersTask = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Initial content"); + })} + }))); + + // Act + Assert.True(setParametersTask.IsCompletedSuccessfully); + _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Changed content"); + })} + }))); + + // Assert + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(0, batch.DisposedComponentIDs.Count); + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => Assert.Empty(diff.Edits), // RootLayout rerendered, but with no changes + diff => + { + // NestedLayout rerendered, patching content in place + Assert.Collection(diff.Edits, edit => + { + Assert.Equal(RenderTreeEditType.UpdateText, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Changed content", + sequence: 0); + }); + }); + } + + [Fact] + public void CanChangeLayout() + { + // Arrange + var setParametersTask1 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(NestedLayout) }, + { nameof(LayoutView.ChildContent), (RenderFragment)(builder => { + builder.AddContent(0, "Some content"); + })} + }))); + Assert.True(setParametersTask1.IsCompletedSuccessfully); + + // Act + var setParametersTask2 = _renderer.Dispatcher.InvokeAsync(() => _layoutViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(LayoutView.Layout), typeof(OtherNestedLayout) }, + }))); + + // Assert + Assert.True(setParametersTask2.IsCompletedSuccessfully); + Assert.Equal(2, _renderer.Batches.Count); + var batch = _renderer.Batches[1]; + Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposes NestedLayout + Assert.Collection(batch.DiffsInOrder, + diff => Assert.Empty(diff.Edits), // LayoutView rerendered, but with no changes + diff => + { + // RootLayout rerendered, changing child + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Component( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + sequence: 0); + }); + }, + diff => + { + // Inserts new OtherNestedLayout + Assert.Collection(diff.Edits, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(0, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout starts here"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(1, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "Some content"); + }, + edit => + { + Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); + Assert.Equal(2, edit.SiblingIndex); + AssertFrame.Text( + batch.ReferenceFrames[edit.ReferenceFrameIndex], + "OtherNestedLayout ends here"); + }); + }); + } + + private class RootLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + if (Body == null) + { + // Prove that we don't expect layouts to tolerate null values for Body + throw new InvalidOperationException("Got a null body when not expecting it"); + } + + builder.AddContent(0, "RootLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "RootLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class NestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "NestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "NestedLayout ends here"); + } + } + + [Layout(typeof(RootLayout))] + private class OtherNestedLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherNestedLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherNestedLayout ends here"); + } + } + } +} diff --git a/src/Components/Components/test/PageDisplayTest.cs b/src/Components/Components/test/PageDisplayTest.cs deleted file mode 100644 index 9b28d95d23c6..000000000000 --- a/src/Components/Components/test/PageDisplayTest.cs +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.RenderTree; -using Microsoft.AspNetCore.Components.Test.Helpers; -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Test -{ - public class PageDisplayTest - { - private TestRenderer _renderer = new TestRenderer(); - private PageDisplay _pageDisplayComponent = new PageDisplay(); - private int _pageDisplayComponentId; - - public PageDisplayTest() - { - _renderer = new TestRenderer(); - _pageDisplayComponent = new PageDisplay(); - _pageDisplayComponentId = _renderer.AssignRootComponentId(_pageDisplayComponent); - } - - [Fact] - public void DisplaysComponentInsideLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - diff => - { - // First is the LayoutDisplay component, which contains a RootLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Component( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex]); - }, - diff => - { - // ... then a RootLayout which contains a ComponentWithLayout - // First is the LayoutDisplay component, which contains a RootLayout - Assert.Collection(diff.Edits, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout starts here"); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - edit => - { - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Text( - batch.ReferenceFrames[edit.ReferenceFrameIndex], - "RootLayout ends here"); - }); - }, - diff => - { - // ... then the ComponentWithLayout - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithLayout)} is here."); - }); - } - - [Fact] - public void DisplaysComponentInsideNestedLayout() - { - // Arrange/Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - var batch = _renderer.Batches.Single(); - Assert.Collection(batch.DiffsInOrder, - // First, a LayoutDisplay containing a RootLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex]), - // Then a RootLayout containing a NestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then a NestedLayout containing a ComponentWithNestedLayout - diff => AssertFrame.Component( - batch.ReferenceFrames[diff.Edits[1].ReferenceFrameIndex]), - // Then the ComponentWithNestedLayout - diff => AssertFrame.Text( - batch.ReferenceFrames[diff.Edits[0].ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here.")); - } - - [Fact] - public void CanChangeDisplayedPageWithSameLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(DifferentComponentWithLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new one - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(DifferentComponentWithLayout)} is here."); - }); - } - - [Fact] - public void CanChangeDisplayedPageWithDifferentLayout() - { - // Arrange - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithLayout) } - }))); - - // Act - _renderer.Dispatcher.InvokeAsync(() => _pageDisplayComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary - { - { nameof(PageDisplay.Page), typeof(ComponentWithNestedLayout) } - }))); - - // Assert - Assert.Equal(2, _renderer.Batches.Count); - var batch = _renderer.Batches[1]; - Assert.Equal(1, batch.DisposedComponentIDs.Count); // Disposed only the inner page component - Assert.Collection(batch.DiffsInOrder, - diff => Assert.Empty(diff.Edits), // LayoutDisplay rerendered, but with no changes - diff => - { - // RootLayout rerendered - Assert.Collection(diff.Edits, - edit => - { - // Removed old page - Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - }, - edit => - { - // Inserted new nested layout - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - Assert.Equal(1, edit.SiblingIndex); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }); - }, - diff => - { - // New nested layout rendered - var edit = diff.Edits[1]; - Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type); - AssertFrame.Component( - batch.ReferenceFrames[edit.ReferenceFrameIndex]); - }, - diff => - { - // New inner page rendered - var singleEdit = diff.Edits.Single(); - Assert.Equal(RenderTreeEditType.PrependFrame, singleEdit.Type); - AssertFrame.Text( - batch.ReferenceFrames[singleEdit.ReferenceFrameIndex], - $"{nameof(ComponentWithNestedLayout)} is here."); - }); - } - - private class RootLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "RootLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "RootLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class NestedLayout : AutoRenderComponent - { - [Parameter] - public RenderFragment Body { get; set; } - - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - builder.AddContent(0, "NestedLayout starts here"); - builder.AddContent(1, Body); - builder.AddContent(2, "NestedLayout ends here"); - } - } - - [Layout(typeof(RootLayout))] - private class ComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithLayout)} is here."); - } - - [Layout(typeof(RootLayout))] - private class DifferentComponentWithLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(DifferentComponentWithLayout)} is here."); - } - - [Layout(typeof(NestedLayout))] - private class ComponentWithNestedLayout : AutoRenderComponent - { - protected override void BuildRenderTree(RenderTreeBuilder builder) - => builder.AddContent(0, $"{nameof(ComponentWithNestedLayout)} is here."); - } - } -} diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs new file mode 100644 index 000000000000..fa92d393fb06 --- /dev/null +++ b/src/Components/Components/test/RouteViewTest.cs @@ -0,0 +1,208 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Test.Helpers; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Test +{ + public class RouteViewTest + { + private readonly TestRenderer _renderer; + private readonly RouteView _routeViewComponent; + private readonly int _routeViewComponentId; + + public RouteViewTest() + { + _renderer = new TestRenderer(); + _routeViewComponent = new RouteView(); + _routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent); + } + + [Fact] + public void ThrowsIfNoRouteDataSupplied() + { + var ex = Assert.Throws(() => + { + // Throws synchronously, so no need to await + _ = _routeViewComponent.SetParametersAsync(ParameterView.Empty); + }); + + + Assert.Equal($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteView.RouteData)}.", ex.Message); + } + + [Fact] + public void RendersPageInsideLayoutView() + { + // Arrange + var routeParams = new Dictionary + { + { nameof(ComponentWithLayout.Message), "Test message" } + }; + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: RouteView renders LayoutView + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + + // Assert: LayoutView renders TestLayout + var layoutViewComponentId = batch.GetComponentFrames().Single().ComponentId; + var layoutViewFrames = _renderer.GetCurrentRenderTreeFrames(layoutViewComponentId).AsEnumerable(); + Assert.Collection(layoutViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 2, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutComponentBase.Body), sequence: 1)); + + // Assert: TestLayout renders page + var testLayoutComponentId = batch.GetComponentFrames().Single().ComponentId; + var testLayoutFrames = _renderer.GetCurrentRenderTreeFrames(testLayoutComponentId).AsEnumerable(); + Assert.Collection(testLayoutFrames, + frame => AssertFrame.Text(frame, "Layout starts here", sequence: 0), + frame => AssertFrame.Region(frame, subtreeLength: 3), + frame => AssertFrame.Component(frame, sequence: 0, subtreeLength: 2), + frame => AssertFrame.Attribute(frame, nameof(ComponentWithLayout.Message), "Test message", sequence: 1), + frame => AssertFrame.Text(frame, "Layout ends here", sequence: 2)); + + // Assert: page itself is rendered, having received parameters from the original route data + var pageComponentId = batch.GetComponentFrames().Single().ComponentId; + var pageFrames = _renderer.GetCurrentRenderTreeFrames(pageComponentId).AsEnumerable(); + Assert.Collection(pageFrames, + frame => AssertFrame.Text(frame, "Hello from the page with message 'Test message'", sequence: 0)); + + // Assert: nothing else was rendered + Assert.Equal(4, batch.DiffsInOrder.Count); + } + + [Fact] + public void UsesDefaultLayoutIfNoneSetOnPage() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses default layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(OtherLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void UsesNoLayoutIfNoneSetOnPageAndNoDefaultSet() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithoutLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + }))); + + // Assert: uses no layout + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)null, sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + [Fact] + public void PageLayoutSupersedesDefaultLayout() + { + // Arrange + var routeParams = new Dictionary(); + var routeData = new RouteData(typeof(ComponentWithLayout), routeParams); + + // Act + _renderer.Dispatcher.InvokeAsync(() => _routeViewComponent.SetParametersAsync(ParameterView.FromDictionary(new Dictionary + { + { nameof(RouteView.RouteData), routeData }, + { nameof(RouteView.DefaultLayout), typeof(OtherLayout) }, + }))); + + // Assert: uses layout specified by page + // Not asserting about what else gets rendered as that's covered by other tests + var batch = _renderer.Batches.Single(); + var routeViewFrames = _renderer.GetCurrentRenderTreeFrames(_routeViewComponentId).AsEnumerable(); + Assert.Collection(routeViewFrames, + frame => AssertFrame.Component(frame, subtreeLength: 3, sequence: 0), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.Layout), (object)typeof(TestLayout), sequence: 1), + frame => AssertFrame.Attribute(frame, nameof(LayoutView.ChildContent), sequence: 2)); + } + + private class ComponentWithoutLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + [Layout(typeof(TestLayout))] + private class ComponentWithLayout : AutoRenderComponent + { + [Parameter] public string Message { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"Hello from the page with message '{Message}'"); + } + } + + private class TestLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "Layout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "Layout ends here"); + } + } + + private class OtherLayout : AutoRenderComponent + { + [Parameter] + public RenderFragment Body { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, "OtherLayout starts here"); + builder.AddContent(1, Body); + builder.AddContent(2, "OtherLayout ends here"); + } + } + } +} diff --git a/src/Components/Shared/test/TestRenderer.cs b/src/Components/Shared/test/TestRenderer.cs index d5ce07bf4b06..135a5cbda278 100644 --- a/src/Components/Shared/test/TestRenderer.cs +++ b/src/Components/Shared/test/TestRenderer.cs @@ -7,6 +7,7 @@ using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -48,6 +49,9 @@ public TestRenderer(IServiceProvider serviceProvider) : base(serviceProvider, Nu public new int AssignRootComponentId(IComponent component) => base.AssignRootComponentId(component); + public new ArrayRange GetCurrentRenderTreeFrames(int componentId) + => base.GetCurrentRenderTreeFrames(componentId); + public void RenderRootComponent(int componentId, ParameterView? parameters = default) { var task = Dispatcher.InvokeAsync(() => base.RenderRootComponentAsync(componentId, parameters ?? ParameterView.Empty)); diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index deaa6bfb640b..655505078a48 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -44,6 +44,7 @@ public void CascadingAuthenticationState_Unauthenticated() Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -56,6 +57,7 @@ public void CascadingAuthenticationState_Authenticated() Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text); Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text); Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -66,6 +68,7 @@ public void AuthorizeViewCases_NoAuthorizationRule_NotAuthorized() WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized")); Browser.Equal("You're not authorized, anonymous", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -75,6 +78,7 @@ public void AuthorizeViewCases_NoAuthorizationRule_Authorized() var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -84,6 +88,7 @@ public void AuthorizeViewCases_RequireRole_Authorized() var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Some User!", () => appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -93,6 +98,7 @@ public void AuthorizeViewCases_RequireRole_NotAuthorized() var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Some User", () => appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -102,6 +108,7 @@ public void AuthorizeViewCases_RequirePolicy_Authorized() var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("Welcome, Bert!", () => appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -111,6 +118,7 @@ public void AuthorizeViewCases_RequirePolicy_NotAuthorized() var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); Browser.Equal("You're not authorized, Mallory", () => appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -120,6 +128,7 @@ public void Router_AllowAnonymous_Anonymous() var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -129,6 +138,7 @@ public void Router_AllowAnonymous_Authenticated() var appElement = MountAndNavigateToAuthTest(PageAllowingAnonymous); Browser.Equal("Welcome to PageAllowingAnonymous!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -138,6 +148,7 @@ public void Router_RequireAuthorization_Authorized() var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Welcome to PageRequiringAuthorization!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -147,6 +158,7 @@ public void Router_RequireAuthorization_NotAuthorized() var appElement = MountAndNavigateToAuthTest(PageRequiringAuthorization); Browser.Equal("Sorry, anonymous, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -156,6 +168,7 @@ public void Router_RequirePolicy_Authorized() var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Welcome to PageRequiringPolicy!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -165,6 +178,7 @@ public void Router_RequirePolicy_NotAuthorized() var appElement = MountAndNavigateToAuthTest(PageRequiringPolicy); Browser.Equal("Sorry, Mallory, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -174,6 +188,7 @@ public void Router_RequireRole_Authorized() var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Welcome to PageRequiringRole!", () => appElement.FindElement(By.CssSelector("#auth-success")).Text); + AssertExpectedLayoutUsed(); } [Fact] @@ -183,6 +198,12 @@ public void Router_RequireRole_NotAuthorized() var appElement = MountAndNavigateToAuthTest(PageRequiringRole); Browser.Equal("Sorry, Bert, you're not authorized.", () => appElement.FindElement(By.CssSelector("#auth-failure")).Text); + AssertExpectedLayoutUsed(); + } + + private void AssertExpectedLayoutUsed() + { + WaitUntilExists(By.Id("auth-links")); } protected IWebElement MountAndNavigateToAuthTest(string authLinkText) diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor index 5ec7bbe529c9..917111085aff 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -8,19 +8,21 @@ and @page authorization rules. *@ - - - Authorizing... - -
- Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. -
-
-
-
- -
- + + + + Authorizing... + +
+ Sorry, @(context.User.Identity.Name ?? "anonymous"), you're not authorized. +
+
+
+
+ +

There's nothing here

+
+
@code { protected override void OnInitialized() diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor similarity index 93% rename from src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor rename to src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor index 35ff333ed7c0..71c040058fa2 100644 --- a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouterLayout.razor @@ -1,3 +1,8 @@ +@inherits LayoutComponentBase + +@Body + +