From 045a487190b1bbab2b03129ba7ba5dbe34194bbf Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Jun 2022 11:44:28 +0200 Subject: [PATCH] V10: Fix sending content notification (#12597) * Add mappers to map between ContentItemDisplay and ContentItemDisplayWithSchedule * Ensure SendingContentNotification is always sent * Add custom setup hook for UmbracoTestServerTestBase * Add test showing bug/fix * Test schedule being mapped correctly * Obsolete the old constructor * Removed TODO --- .../ContentEditing/ContentItemDisplay.cs | 6 +- .../OutgoingEditorModelEventAttribute.cs | 50 ++- .../Mapping/ContentMapDefinition.cs | 130 +++++- .../UmbracoTestServerTestBase.cs | 401 +++++++++--------- .../OutgoingEditorModelEventFilterTests.cs | 163 +++++++ 5 files changed, 543 insertions(+), 207 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index e2fcf71053..eb800791a2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -17,9 +17,9 @@ public class ContentItemDisplayWithSchedule : ContentItemDisplay [DataContract(Name = "content", Namespace = "")] -public class - ContentItemDisplay : INotificationModel, - IErrorModel // ListViewAwareContentItemDisplayBase +public class ContentItemDisplay : + INotificationModel, + IErrorModel // ListViewAwareContentItemDisplayBase where TVariant : ContentVariantDisplay { public ContentItemDisplay() diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 8c4db1a041..c1f9dbac4e 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -1,13 +1,16 @@ using System.Collections; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Filters; @@ -25,22 +28,34 @@ internal sealed class OutgoingEditorModelEventAttribute : TypeFilterAttribute private class OutgoingEditorModelEventFilter : IActionFilter { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IEventAggregator _eventAggregator; - + private readonly IUmbracoMapper _mapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + [ActivatorUtilitiesConstructor] + public OutgoingEditorModelEventFilter( + IUmbracoContextAccessor umbracoContextAccessor, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IEventAggregator eventAggregator, + IUmbracoMapper mapper) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _backOfficeSecurityAccessor = backOfficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backOfficeSecurityAccessor)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _mapper = mapper; + } + + [Obsolete("Please use constructor that takes an IUmbracoMapper, scheduled for removal in V12")] public OutgoingEditorModelEventFilter( IUmbracoContextAccessor umbracoContextAccessor, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IEventAggregator eventAggregator) + : this( + umbracoContextAccessor, + backOfficeSecurityAccessor, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) { - _umbracoContextAccessor = umbracoContextAccessor - ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _backOfficeSecurityAccessor = backOfficeSecurityAccessor - ?? throw new ArgumentNullException(nameof(backOfficeSecurityAccessor)); - _eventAggregator = eventAggregator - ?? throw new ArgumentNullException(nameof(eventAggregator)); } public void OnActionExecuted(ActionExecutedContext context) @@ -77,6 +92,25 @@ internal sealed class OutgoingEditorModelEventAttribute : TypeFilterAttribute case ContentItemDisplay content: _eventAggregator.Publish(new SendingContentNotification(content, umbracoContext)); break; + case ContentItemDisplayWithSchedule contentWithSchedule: + // This is a bit weird, since ContentItemDisplayWithSchedule was introduced later, + // the SendingContentNotification only accepts ContentItemDisplay, + // which means we have to map it to this before sending the notification. + ContentItemDisplay? display = _mapper.Map(contentWithSchedule); + if (display is null) + { + // This will never happen. + break; + } + + // Now that the display is mapped to the non-schedule one we can publish the notification. + _eventAggregator.Publish(new SendingContentNotification(display, umbracoContext)); + + // We want the changes the handler makes to take effect. + // So we have to map these changes back to the existing ContentItemWithSchedule. + // To avoid losing the schedule information we add the old variants to context. + _mapper.Map(display, contentWithSchedule, mapperContext => mapperContext.Items[nameof(contentWithSchedule.Variants)] = contentWithSchedule.Variants); + break; case MediaItemDisplay media: _eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext)); break; diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index f93512863b..2e2a70f29c 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -95,16 +95,138 @@ internal class ContentMapDefinition : IMapDefinition public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define>( - (source, context) => new ContentItemBasic(), Map); + mapper.Define>((source, context) => new ContentItemBasic(), Map); mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); mapper.Define((source, context) => new ContentItemDisplay(), Map); - mapper.Define( - (source, context) => new ContentItemDisplayWithSchedule(), Map); + mapper.Define((source, context) => new ContentItemDisplayWithSchedule(), Map); mapper.Define((source, context) => new ContentVariantDisplay(), Map); mapper.Define((source, context) => new ContentVariantScheduleDisplay(), Map); + + mapper.Define((source, context) => new ContentItemDisplay(), Map); + mapper.Define((source, context) => new ContentItemDisplayWithSchedule(), Map); + + mapper.Define((source, context) => new ContentVariantScheduleDisplay(), Map); + mapper.Define((source, context) => new ContentVariantDisplay(), Map); + } + + // Umbraco.Code.MapAll + private void Map(ContentVariantScheduleDisplay source, ContentVariantDisplay target, MapperContext context) + { + target.CreateDate = source.CreateDate; + target.DisplayName = source.DisplayName; + target.Language = source.Language; + target.Name = source.Name; + target.PublishDate = source.PublishDate; + target.Segment = source.Segment; + target.State = source.State; + target.Tabs = source.Tabs; + target.UpdateDate = source.UpdateDate; + } + + // Umbraco.Code.MapAll + private void Map(ContentItemDisplay source, ContentItemDisplayWithSchedule target, MapperContext context) + { + target.AllowedActions = source.AllowedActions; + target.AllowedTemplates = source.AllowedTemplates; + target.AllowPreview = source.AllowPreview; + target.ContentApps = source.ContentApps; + target.ContentDto = source.ContentDto; + target.ContentTypeAlias = source.ContentTypeAlias; + target.ContentTypeId = source.ContentTypeId; + target.ContentTypeKey = source.ContentTypeKey; + target.ContentTypeName = source.ContentTypeName; + target.DocumentType = source.DocumentType; + target.Errors = source.Errors; + target.Icon = source.Icon; + target.Id = source.Id; + target.IsBlueprint = source.IsBlueprint; + target.IsChildOfListView = source.IsChildOfListView; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Owner = source.Owner; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PersistedContent = source.PersistedContent; + target.SortOrder = source.SortOrder; + target.TemplateAlias = source.TemplateAlias; + target.TemplateId = source.TemplateId; + target.Trashed = source.Trashed; + target.TreeNodeUrl = source.TreeNodeUrl; + target.Udi = source.Udi; + target.UpdateDate = source.UpdateDate; + target.Updater = source.Updater; + target.Urls = source.Urls; + target.Variants = context.MapEnumerable(source.Variants); + } + + // Umbraco.Code.MapAll + private void Map(ContentVariantDisplay source, ContentVariantScheduleDisplay target, MapperContext context) + { + target.CreateDate = source.CreateDate; + target.DisplayName = source.DisplayName; + target.Language = source.Language; + target.Name = source.Name; + target.PublishDate = source.PublishDate; + target.Segment = source.Segment; + target.State = source.State; + target.Tabs = source.Tabs; + target.UpdateDate = source.UpdateDate; + + // We'll only try and map the ReleaseDate/ExpireDate if the "old" ContentVariantScheduleDisplay is in the context, otherwise we'll just skip it quietly. + _ = context.Items.TryGetValue(nameof(ContentItemDisplayWithSchedule.Variants), out var variants); + if (variants is IEnumerable scheduleDisplays) + { + ContentVariantScheduleDisplay? item = scheduleDisplays.FirstOrDefault(x => x.Language?.Id == source.Language?.Id && x.Segment == source.Segment); + + if (item is null) + { + // If we can't find the old variants display, we'll just not try and map it. + return; + } + + target.ReleaseDate = item.ReleaseDate; + target.ExpireDate = item.ExpireDate; + } + } + + // Umbraco.Code.MapAll + private static void Map(ContentItemDisplayWithSchedule source, ContentItemDisplay target, MapperContext context) + { + target.AllowedActions = source.AllowedActions; + target.AllowedTemplates = source.AllowedTemplates; + target.AllowPreview = source.AllowPreview; + target.ContentApps = source.ContentApps; + target.ContentDto = source.ContentDto; + target.ContentTypeAlias = source.ContentTypeAlias; + target.ContentTypeId = source.ContentTypeId; + target.ContentTypeKey = source.ContentTypeKey; + target.ContentTypeName = source.ContentTypeName; + target.DocumentType = source.DocumentType; + target.Errors = source.Errors; + target.Icon = source.Icon; + target.Id = source.Id; + target.IsBlueprint = source.IsBlueprint; + target.IsChildOfListView = source.IsChildOfListView; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Owner = source.Owner; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PersistedContent = source.PersistedContent; + target.SortOrder = source.SortOrder; + target.TemplateAlias = source.TemplateAlias; + target.TemplateId = source.TemplateId; + target.Trashed = source.Trashed; + target.TreeNodeUrl = source.TreeNodeUrl; + target.Udi = source.Udi; + target.UpdateDate = source.UpdateDate; + target.Updater = source.Updater; + target.Urls = source.Urls; + target.Variants = source.Variants; } // Umbraco.Code.MapAll diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 8cb9262ba2..823754bdfc 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -29,231 +30,247 @@ using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; -namespace Umbraco.Cms.Tests.Integration.TestServerTest; - -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] -public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase +namespace Umbraco.Cms.Tests.Integration.TestServerTest { - [SetUp] - public void Setup() + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] + public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase { - /* - * It's worth noting that our usage of WebApplicationFactory is non-standard, - * the intent is that your Startup.ConfigureServices is called just like - * when the app starts up, then replacements are registered in this class with - * builder.ConfigureServices (builder.ConfigureTestServices has hung around from before the - * generic host switchover). - * - * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but - * we should get there one day. - * - * However we need to separate the testing framework we provide for downstream projects from our own tests. - * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. - * - * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests - */ - var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + protected HttpClient Client { get; private set; } - // additional host configuration for web server integration tests - Factory = factory.WithWebHostBuilder(builder => + protected LinkGenerator LinkGenerator { get; private set; } + + protected WebApplicationFactory Factory { get; private set; } + + /// + /// Hook for altering UmbracoBuilder setup + /// + /// + /// Can also be used for registering test doubles. + /// + protected virtual void CustomTestSetup(IUmbracoBuilder builder) { - // Otherwise inferred as $(SolutionDir)/Umbraco.Tests.Integration (note lack of src/tests) - builder.UseContentRoot(Assembly.GetExecutingAssembly().GetRootDirectorySafe()); + } - // Executes after the standard ConfigureServices method - builder.ConfigureTestServices(services => - - // Add a test auth scheme with a test auth handler to authn and assign the user - services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) - .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); - }); - - Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - LinkGenerator = Factory.Services.GetRequiredService(); - } - - protected HttpClient Client { get; private set; } - - protected LinkGenerator LinkGenerator { get; private set; } - - protected WebApplicationFactory Factory { get; private set; } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareApiControllerUrl(Expression> methodSelector) - where T : UmbracoApiController - { - var url = LinkGenerator.GetUmbracoApiService(methodSelector); - return PrepareUrl(url); - } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareSurfaceControllerUrl(Expression> methodSelector) - where T : SurfaceController - { - var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); - return PrepareUrl(url); - } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareUrl(string url) - { - var umbracoContextFactory = GetRequiredService(); - var httpContextAccessor = GetRequiredService(); - - httpContextAccessor.HttpContext = new DefaultHttpContext + [SetUp] + public void Setup() { - Request = + /* + * It's worth noting that our usage of WebApplicationFactory is non-standard, + * the intent is that your Startup.ConfigureServices is called just like + * when the app starts up, then replacements are registered in this class with + * builder.ConfigureServices (builder.ConfigureTestServices has hung around from before the + * generic host switchover). + * + * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but + * we should get there one day. + * + * However we need to separate the testing framework we provide for downstream projects from our own tests. + * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. + * + * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests + */ + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + + // additional host configuration for web server integration tests + Factory = factory.WithWebHostBuilder(builder => { - Scheme = "https", - Host = new HostString("localhost", 80), - Path = url, - QueryString = new QueryString(string.Empty) - } - }; + // Otherwise inferred as $(SolutionDir)/Umbraco.Tests.Integration (note lack of src/tests) + builder.UseContentRoot(Assembly.GetExecutingAssembly().GetRootDirectorySafe()); - umbracoContextFactory.EnsureUmbracoContext(); + // Executes after the standard ConfigureServices method + builder.ConfigureTestServices(services => - return url; - } + // Add a test auth scheme with a test auth handler to authn and assign the user + services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) + .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); + }); - private IHostBuilder CreateHostBuilder() - { - var hostBuilder = Host.CreateDefaultBuilder() - .ConfigureUmbracoDefaults() - .ConfigureAppConfiguration((context, configBuilder) => + Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { - context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); - configBuilder.Sources.Clear(); - configBuilder.AddInMemoryCollection(InMemoryConfiguration); - configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); + AllowAutoRedirect = false + }); - Configuration = configBuilder.Build(); - }) - .ConfigureWebHost(builder => + LinkGenerator = Factory.Services.GetRequiredService(); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareApiControllerUrl(Expression> methodSelector) + where T : UmbracoApiController + { + var url = LinkGenerator.GetUmbracoApiService(methodSelector); + return PrepareUrl(url); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareSurfaceControllerUrl(Expression> methodSelector) + where T : SurfaceController + { + var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); + return PrepareUrl(url); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareUrl(string url) + { + IUmbracoContextFactory umbracoContextFactory = GetRequiredService(); + IHttpContextAccessor httpContextAccessor = GetRequiredService(); + + httpContextAccessor.HttpContext = new DefaultHttpContext { - builder.ConfigureServices((context, services) => + Request = + { + Scheme = "https", + Host = new HostString("localhost", 80), + Path = url, + QueryString = new QueryString(string.Empty) + } + }; + + umbracoContextFactory.EnsureUmbracoContext(); + + return url; + } + + private IHostBuilder CreateHostBuilder() + { + IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureUmbracoDefaults() + .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + configBuilder.Sources.Clear(); + configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); - ConfigureServices(services); - ConfigureTestServices(services); - services.AddUnique(CreateLoggerFactory()); - - if (!TestOptions.Boot) + Configuration = configBuilder.Build(); + }) + .ConfigureWebHost(builder => + { + builder.ConfigureServices((context, services) => { - // If boot is false, we don't want the CoreRuntime hosted service to start - // So we replace it with a Mock - services.AddUnique(Mock.Of()); - } + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + + ConfigureServices(services); + ConfigureTestServices(services); + services.AddUnique(CreateLoggerFactory()); + + if (!TestOptions.Boot) + { + // If boot is false, we don't want the CoreRuntime hosted service to start + // So we replace it with a Mock + services.AddUnique(Mock.Of()); + } + }); + + // call startup + builder.Configure(Configure); + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; }); - // call startup - builder.Configure(Configure); - }) - .UseDefaultServiceProvider(cfg => - { - // These default to true *if* WebHostEnvironment.EnvironmentName == Development - // When running tests, EnvironmentName used to be null on the mock that we register into services. - // Enable opt in for tests so that validation occurs regardless of environment name. - // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. - cfg.ValidateOnBuild = true; - cfg.ValidateScopes = true; - }); + return hostBuilder; + } - return hostBuilder; - } + protected virtual IServiceProvider Services => Factory.Services; - protected virtual IServiceProvider Services => Factory.Services; + protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); - protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); + protected void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); - protected void ConfigureServices(IServiceCollection services) - { - services.AddTransient(); + Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); - var hostingEnvironment = TestHelper.GetHostingEnvironment(); + TypeLoader typeLoader = services.AddTypeLoader( + GetType().Assembly, + hostingEnvironment, + TestHelper.ConsoleLoggerFactory, + AppCaches.NoCache, + Configuration, + TestHelper.Profiler); - var typeLoader = services.AddTypeLoader( - GetType().Assembly, - hostingEnvironment, - TestHelper.ConsoleLoggerFactory, - AppCaches.NoCache, - Configuration, - TestHelper.Profiler); + services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); - services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); + var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); - var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); + builder + .AddConfiguration() + .AddUmbracoCore() + .AddWebComponents() + .AddNuCache() + .AddRuntimeMinifier() + .AddBackOfficeCore() + .AddBackOfficeAuthentication() + .AddBackOfficeIdentity() + .AddMembersIdentity() + .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) + .AddPreviewSupport() + .AddMvcAndRazor(mvcBuilding: mvcBuilder => + { + // Adds Umbraco.Web.BackOffice + mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); - builder - .AddConfiguration() - .AddUmbracoCore() - .AddWebComponents() - .AddNuCache() - .AddRuntimeMinifier() - .AddBackOfficeCore() - .AddBackOfficeAuthentication() - .AddBackOfficeIdentity() - .AddMembersIdentity() - .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) - .AddPreviewSupport() - .AddMvcAndRazor(mvcBuilder => - { - // Adds Umbraco.Web.BackOffice - mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); + // Adds Umbraco.Web.Common + mvcBuilder.AddApplicationPart(typeof(RenderController).Assembly); - // Adds Umbraco.Web.Common - mvcBuilder.AddApplicationPart(typeof(RenderController).Assembly); + // Adds Umbraco.Web.Website + mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); - // Adds Umbraco.Web.Website - mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); + // Adds Umbraco.Tests.Integration + mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); + }) + .AddWebServer() + .AddWebsite() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() + .AddTestServices(TestHelper); // This is the important one! - // Adds Umbraco.Tests.Integration - mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); - }) - .AddWebServer() - .AddWebsite() - .AddUmbracoSqlServerSupport() - .AddUmbracoSqliteSupport() - .AddTestServices(TestHelper) // This is the important one! - .Build(); - } + CustomTestSetup(builder); + builder.Build(); + } - /// - /// Hook for registering test doubles. - /// - protected virtual void ConfigureTestServices(IServiceCollection services) - { - } + /// + /// Hook for registering test doubles. + /// + protected virtual void ConfigureTestServices(IServiceCollection services) + { + } - protected void Configure(IApplicationBuilder app) - { - UseTestDatabase(app); + protected void Configure(IApplicationBuilder app) + { + UseTestDatabase(app); - app.UseUmbraco() - .WithMiddleware(u => - { - u.UseBackOffice(); - u.UseWebsite(); - }) - .WithEndpoints(u => - { - u.UseBackOfficeEndpoints(); - u.UseWebsiteEndpoints(); - }); + app.UseUmbraco() + .WithMiddleware(u => + { + u.UseBackOffice(); + u.UseWebsite(); + }) + .WithEndpoints(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); + } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs new file mode 100644 index 0000000000..e627a3300f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.BackOffice.Controllers; +using Umbraco.Cms.Web.Common.Formatters; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Filters; + +[TestFixture] +public class OutgoingEditorModelEventFilterTests : UmbracoTestServerTestBase +{ + private static int _messageCount; + private static Action _handler; + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + } + + [TearDown] + public void Reset() => ResetNotifications(); + + [Test] + public async Task Content_Item_With_Schedule_Raises_SendingContentNotification() + { + IContentTypeService contentTypeService = GetRequiredService(); + IContentService contentService = GetRequiredService(); + IJsonSerializer serializer = GetRequiredService(); + + var contentType = new ContentTypeBuilder().Build(); + contentTypeService.Save(contentType); + + var contentToRequest = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .Build(); + + contentService.Save(contentToRequest); + + _handler = notification => notification.Content.AllowPreview = false; + + var url = PrepareApiControllerUrl(x => x.GetById(contentToRequest.Id)); + + HttpResponseMessage response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + var text = await response.Content.ReadAsStringAsync(); + text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var display = serializer.Deserialize(text); + + Assert.AreEqual(1, _messageCount); + Assert.IsNotNull(display); + Assert.IsFalse(display.AllowPreview); + } + + [Test] + public async Task Publish_Schedule_Is_Mapped_Correctly() + { + const string UsIso = "en-US"; + const string DkIso = "da-DK"; + const string SweIso = "sv-SE"; + var contentTypeService = GetRequiredService(); + var contentService = GetRequiredService(); + var localizationService = GetRequiredService(); + IJsonSerializer serializer = GetRequiredService(); + + var contentType = new ContentTypeBuilder() + .WithContentVariation(ContentVariation.Culture) + .Build(); + contentTypeService.Save(contentType); + + var dkLang = new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build(); + + var sweLang = new LanguageBuilder() + .WithCultureInfo(SweIso) + .WithIsDefault(false) + .Build(); + + localizationService.Save(dkLang); + localizationService.Save(sweLang); + + var content = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithCultureName(UsIso, "Same Name") + .WithCultureName(SweIso, "Same Name") + .WithCultureName(DkIso, "Same Name") + .Build(); + + contentService.Save(content); + var schedule = new ContentScheduleCollection(); + + var dkReleaseDate = new DateTime(2022, 06, 22, 21, 30, 42); + var dkExpireDate = new DateTime(2022, 07, 15, 18, 00, 00); + + var sweReleaseDate = new DateTime(2022, 06, 23, 22, 30, 42); + var sweExpireDate = new DateTime(2022, 07, 10, 14, 20, 00); + schedule.Add(DkIso, dkReleaseDate, dkExpireDate); + schedule.Add(SweIso, sweReleaseDate, sweExpireDate); + contentService.PersistContentSchedule(content, schedule); + + var url = PrepareApiControllerUrl(x => x.GetById(content.Id)); + + HttpResponseMessage response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + var text = await response.Content.ReadAsStringAsync(); + text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var display = serializer.Deserialize(text); + + Assert.IsNotNull(display); + Assert.AreEqual(1, _messageCount); + + var dkVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == DkIso); + Assert.IsNotNull(dkVariant); + Assert.AreEqual(dkReleaseDate, dkVariant.ReleaseDate); + Assert.AreEqual(dkExpireDate, dkVariant.ExpireDate); + + var sweVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == SweIso); + Assert.IsNotNull(sweVariant); + Assert.AreEqual(sweReleaseDate, sweVariant.ReleaseDate); + Assert.AreEqual(sweExpireDate, sweVariant.ExpireDate); + + var usVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == UsIso); + Assert.IsNotNull(usVariant); + Assert.IsNull(usVariant.ReleaseDate); + Assert.IsNull(usVariant.ExpireDate); + } + + private void ResetNotifications() + { + _messageCount = 0; + _handler = null; + } + + private class FilterEventHandler : INotificationHandler + { + public void Handle(SendingContentNotification notification) + { + _messageCount += 1; + _handler?.Invoke(notification); + } + } +}