diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 049d7477e7..f621d27fa6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -719,6 +719,7 @@ af Fortryd Celle margen + Skift Vælg Ryd Luk diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index c7ad610ef0..38c4916e31 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -759,6 +759,7 @@ by Cancel Cell margin + Change Choose Clear Close diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 9325f9e8ae..6dbd5f1e79 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -704,9 +704,6 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [DataMember(Name = "packageFolder")] public string? PackageFolder { get; set; } - - [DataMember(Name = "sectionAlias")] - public string? SectionAlias { get; set; } } private IEnumerable GetPluginTrees() @@ -738,7 +735,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers continue; } - yield return new PluginTree { Alias = tree.TreeAlias, PackageFolder = pluginController.AreaName, SectionAlias = tree.SectionAlias }; + yield return new PluginTree { Alias = tree.TreeAlias, PackageFolder = pluginController.AreaName }; } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index a269c3605a..00b924db5d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -583,10 +583,10 @@ public class MediaController : ContentControllerBase Directory.CreateDirectory(root); //must have a file - if (file.Count == 0) + if (file is null || file.Count == 0) { _postAddFileSemaphore.Release(); - return NotFound(); + return NotFound("No file was uploaded"); } //get the string json from the request @@ -769,12 +769,26 @@ public class MediaController : ContentControllerBase break; } - // If media type is still File then let's check if it's an image. + // If media type is still File then let's check if it's an imageor a custom image type. if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && _imageUrlGenerator.IsSupportedImageFormat(ext)) + { + if (allowedContentTypes.Any(mt => mt.Alias == Constants.Conventions.MediaTypes.Image)) { mediaTypeAlias = Constants.Conventions.MediaTypes.Image; } + else + { + IMediaType? customType = allowedContentTypes.FirstOrDefault(mt => + mt.CompositionPropertyTypes.Any(pt => + pt.PropertyEditorAlias == Constants.PropertyEditors.Aliases.ImageCropper)); + + if (customType is not null) + { + mediaTypeAlias = customType.Alias; + } + } + } } else { diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 28f685bf0d..461d1fc82f 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -360,7 +360,7 @@ public class ApplicationTreeController : UmbracoAuthorizedApiController ControllerActionDescriptor? actionDescriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items .Cast() .First(x => - (x.ControllerTypeInfo.FullName ?? string.Empty).Equals(controllerType.FullName) && + x.ControllerName.Equals(controllerName) && x.ActionName == action); var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor); diff --git a/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs index 0c46e809d2..630584a839 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ScriptsTreeController.cs @@ -3,12 +3,10 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; -using Umbraco.Cms.Web.Common.Attributes; namespace Umbraco.Cms.Web.BackOffice.Trees; [CoreTree] -[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, TreeTitle = "Scripts", SortOrder = 10, TreeGroup = Constants.Trees.Groups.Templating)] public class ScriptsTreeController : FileSystemTreeController { diff --git a/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs index 32f2a3e465..3ff7a7ecfc 100644 --- a/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/StylesheetsTreeController.cs @@ -3,12 +3,10 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; -using Umbraco.Cms.Web.Common.Attributes; namespace Umbraco.Cms.Web.BackOffice.Trees; [CoreTree] -[PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [Tree(Constants.Applications.Settings, Constants.Trees.Stylesheets, TreeTitle = "Stylesheets", SortOrder = 9, TreeGroup = Constants.Trees.Groups.Templating)] public class StylesheetsTreeController : FileSystemTreeController { diff --git a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs index d8506f5692..1688a99ec2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Trees/UrlHelperExtensions.cs @@ -47,15 +47,14 @@ public static class UrlHelperExtensions string nodeId, FormCollection? queryStrings) { - var actionName = "GetNodes"; - var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, treeType); - actionUrl = StartOrContinueQueryString(actionUrl, actionName); - - // Now we need to append the query strings - // Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter - // that should only ever be used when requesting a tree to render (root), not a tree node - actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id", TreeQueryStringParameters.StartNodeId); + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetNodes", treeType)? + .EnsureEndsWith('?'); + //now we need to append the query strings + actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id", + //Always ignore the custom start node id when generating URLs for tree nodes since this is a custom once-only parameter + // that should only ever be used when requesting a tree to render (root), not a tree node + TreeQueryStringParameters.StartNodeId); return actionUrl; } @@ -66,29 +65,11 @@ public static class UrlHelperExtensions string nodeId, FormCollection? queryStrings) { - var actionName = "GetMenu"; - var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, treeType); - actionUrl = StartOrContinueQueryString(actionUrl, actionName); + var actionUrl = urlHelper.GetUmbracoApiService(umbracoApiControllerTypeCollection, "GetMenu", treeType)? + .EnsureEndsWith('?'); - // now we need to append the query strings + //now we need to append the query strings actionUrl += "id=" + nodeId.EnsureEndsWith('&') + queryStrings?.ToQueryString("id"); return actionUrl; } - - /// - /// Check the provided string already includes a querystring fragment - /// If so, result has "&" appended, else has "?" appended. - /// - /// - /// - /// - private static string? StartOrContinueQueryString(string? actionUrl, string? delimiter) - { - if (actionUrl is null) - { - return actionUrl; - } - - return actionUrl.EnsureEndsWith(actionUrl.Contains($"{delimiter}?") ? "&" : "?"); - } } diff --git a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs index 95c4ae5cec..2f56cdb51f 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoMemberAuthorizeFilter.cs @@ -60,14 +60,14 @@ public class UmbracoMemberAuthorizeFilter : IAsyncAuthorizationFilter { context.HttpContext.SetReasonPhrase( "Resource restricted: the member is not of a permitted type or group."); + context.HttpContext.Response.StatusCode = 403; context.Result = new ForbidResult(); } } else { - context.HttpContext.SetReasonPhrase( - "Resource restricted: the member is not logged in."); - context.Result = new UnauthorizedResult(); + context.HttpContext.Response.StatusCode = 401; + context.Result = new ForbidResult(); } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index b8c2874641..1ba9a52526 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -1,10 +1,12 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security; @@ -58,7 +60,16 @@ public sealed class ConfigureMemberCookieOptions : IConfigureNamedOptions { - ctx.Response.StatusCode = StatusCodes.Status403Forbidden; + // When the controller is an UmbracoAPIController, we want to return a StatusCode instead of a redirect. + // All other cases should use the default Redirect of the CookieAuthenticationEvent. + var controllerDescriptor = ctx.HttpContext.GetEndpoint()?.Metadata + .OfType() + .FirstOrDefault(); + + if (!controllerDescriptor?.ControllerTypeInfo.IsSubclassOf(typeof(UmbracoApiController)) ?? false) + { + new CookieAuthenticationEvents().OnRedirectToAccessDenied(ctx); + } return Task.CompletedTask; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js index 4b8e8fa146..51029234f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js @@ -32,7 +32,7 @@ Umbraco.Sys.ServerVariables = { }, umbracoPlugins: { trees: [ - { alias: "myTree", packageFolder: "MyPackage", sectionAlias: "myPackageSectionAlias" } + { alias: "myTree", packageFolder: "MyPackage" } ] }, isDebuggingEnabled: true, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index abf173b129..238d9a8ee6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -344,10 +344,14 @@ function clipboardService($window, notificationsService, eventsService, localSto // Clean up each entry var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); - // remove previous copies of this entry: + // remove previous copies of this entry (Make sure to not remove copies from unsaved content): storage.entries = storage.entries.filter( (entry) => { - return entry.unique !== uniqueKey; + if (entry.unique === 0) { + return displayLabel !== entry.label; + } else { + return entry.unique !== uniqueKey; + } } ); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 248e78880a..77b97545b6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -616,7 +616,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService if (!treeAlias) { throw "Could not get tree alias for node " + args.node.id; } - templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias, args.node.section); + templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias); } setMode("dialog"); @@ -633,7 +633,6 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * * @param {string} treeAlias the alias of the tree to look up * @param {string} action the view file name - * @param {string} sectionAlias the alias of the current section * @description * creates the templateUrl based on treeAlias and action * by convention we will look into the /views/{treetype}/{action}.html @@ -641,8 +640,8 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * we will also check for a 'packageName' for the current tree, if it exists then the convention will be: * for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html */ - getTreeTemplateUrl: function (treeAlias, action, sectionAlias) { - var packageTreeFolder = treeService.getTreePackageFolder(treeAlias, sectionAlias); + getTreeTemplateUrl: function (treeAlias, action) { + var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); if (packageTreeFolder) { return (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + "/" + packageTreeFolder + diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index fa5d297a88..ba9ebc1b00 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -166,27 +166,23 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS * * @description * Determines if the current tree is a plugin tree and if so returns the package folder it has declared - * so we know where to find its views, otherwise it will just return undefined. + * so we know where to find it's views, otherwise it will just return undefined. * * @param {String} treeAlias The tree alias to check - * @param {String} sectionAlias The current section */ - getTreePackageFolder: function (treeAlias, sectionAlias) { + getTreePackageFolder: function (treeAlias) { //we determine this based on the server variables - if (!Umbraco.Sys.ServerVariables.umbracoPlugins || !Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { - return undefined; - } + if (Umbraco.Sys.ServerVariables.umbracoPlugins && + Umbraco.Sys.ServerVariables.umbracoPlugins.trees && + Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { - let found; - if (sectionAlias !== undefined) { - found = Umbraco.Sys.ServerVariables.umbracoPlugins.trees.find(item => - invariantEquals(item.alias, treeAlias) && invariantEquals(item.sectionAlias, sectionAlias)); - } else { - found = Umbraco.Sys.ServerVariables.umbracoPlugins.trees.find(item => - invariantEquals(item.alias, treeAlias)); - } + var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { + return invariantEquals(item.alias, treeAlias); + }); - return found ? found.packageFolder : undefined; + return found ? found.packageFolder : undefined; + } + return undefined; }, /** @@ -872,7 +868,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //start var wrappedPromise = doSync(); - //then wrap it + //then wrap it wrappedPromise.then(function (args) { deferred.resolve(args); }, function (args) { diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index e6d97ea18f..7e65346d1b 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -1,5 +1,5 @@ window.app.config(function ($routeProvider) { - + /** * This determines if the route can continue depending on authentication and initialization requirements * @param {boolean} authRequired If true, it checks if the user is authenticated and will resolve successfully @@ -117,9 +117,9 @@ window.app.config(function ($routeProvider) { template: "
", //This controller will execute for this route, then we can execute some code in order to set the template Url controller: function ($scope, $route, $routeParams, $location, sectionService) { - + //We are going to check the currently loaded sections for the user and if the section we are navigating - //to has a custom route path we'll use that + //to has a custom route path we'll use that sectionService.getSectionsForUser().then(function(sections) { //find the one we're requesting var found = _.find(sections, function(s) { @@ -175,9 +175,8 @@ window.app.config(function ($routeProvider) { if ($routeParams.section.toLowerCase() === "users" && $routeParams.tree.toLowerCase() === "users" && usersPages.indexOf($routeParams.method.toLowerCase()) === -1) { $scope.templateUrl = "views/users/overview.html"; return; - } - - $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method, $routeParams.section); + } + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, resolve: canRoute(true) @@ -191,9 +190,8 @@ window.app.config(function ($routeProvider) { if (!$routeParams.tree || !$routeParams.method) { $scope.templateUrl = "views/common/dashboard.html"; return; - } - - $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method, $routeParams.section); + } + $scope.templateUrl = navigationService.getTreeTemplateUrl($routeParams.tree, $routeParams.method); }, reloadOnSearch: false, reloadOnUrl: false, @@ -201,7 +199,7 @@ window.app.config(function ($routeProvider) { }) .otherwise({ redirectTo: '/login' }); }).config(function ($locationProvider) { - + $locationProvider.html5Mode(false); //turn html5 mode off $locationProvider.hashPrefix(''); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html index 56c7a9cf48..be6f21ed96 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html @@ -5,7 +5,7 @@ ng-repeat="subView in subViews track by subView.alias" ng-class="'sub-view-' + subView.name" val-sub-view="subView" - ng-if="subView.active" + ng-show="subView.active" >
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js index 63800a9e12..4d19cf557a 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/tree-service.spec.js @@ -298,13 +298,13 @@ describe('tree service tests', function () { it('can find a plugin based tree', function () { //we know this exists in the mock umbraco server vars - var found = treeService.getTreePackageFolder("myTree", "MyPackageSectionAlias"); + var found = treeService.getTreePackageFolder("myTree"); expect(found).toBe("MyPackage"); }); it('returns undefined for a not found tree', function () { //we know this does not exist in the mock umbraco server vars - var found = treeService.getTreePackageFolder("asdfasdf", "fdsafdsa"); + var found = treeService.getTreePackageFolder("asdfasdf"); expect(found).not.toBeDefined(); }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs new file mode 100644 index 0000000000..0fc1dfa85d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs @@ -0,0 +1,126 @@ +using System.Net; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Cms.Web.Website.Controllers; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.Website.Security +{ + public class MemberAuthorizeTests : UmbracoTestServerTestBase + { + private Mock _memberManagerMock = new(); + + protected override void ConfigureTestServices(IServiceCollection services) + { + _memberManagerMock = new Mock(); + services.Remove(new ServiceDescriptor(typeof(IMemberManager), typeof(MemberManager), ServiceLifetime.Scoped)); + services.Remove(new ServiceDescriptor(typeof(MemberManager), ServiceLifetime.Scoped)); + services.AddScoped(_ => _memberManagerMock.Object); + } + + [Test] + public async Task Secure_SurfaceController_Should_Return_Redirect_WhenNotLoggedIn() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(false); + + var url = PrepareSurfaceControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + var cookieAuthenticationOptions = Services.GetService>(); + Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); + Assert.AreEqual(cookieAuthenticationOptions.Value.AccessDeniedPath.ToString(), response.Headers.Location?.AbsolutePath); + } + + [Test] + public async Task Secure_SurfaceController_Should_Return_Redirect_WhenNotAuthorized() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(true); + _memberManagerMock.Setup(x => x.IsMemberAuthorizedAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(false); + + var url = PrepareSurfaceControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + var cookieAuthenticationOptions = Services.GetService>(); + Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); + Assert.AreEqual(cookieAuthenticationOptions.Value.AccessDeniedPath.ToString(), response.Headers.Location?.AbsolutePath); + } + + + [Test] + public async Task Secure_ApiController_Should_Return_Unauthorized_WhenNotLoggedIn() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(false); + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Test] + public async Task Secure_ApiController_Should_Return_Forbidden_WhenNotAuthorized() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(true); + _memberManagerMock.Setup(x => x.IsMemberAuthorizedAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(false); + + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + } + + public class TestSurfaceController : SurfaceController + { + public TestSurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) + { + } + + [UmbracoMemberAuthorize] + public IActionResult Secure() => NoContent(); + } + + public class TestApiController : UmbracoApiController + { + [UmbracoMemberAuthorize] + public IActionResult Secure() => NoContent(); + } +}