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
This commit is contained in:
Mole
2022-06-21 11:44:28 +02:00
committed by GitHub
parent 1ab472472c
commit 045a487190
5 changed files with 543 additions and 207 deletions

View File

@@ -17,9 +17,9 @@ public class ContentItemDisplayWithSchedule : ContentItemDisplay<ContentVariantS
/// A model representing a content item to be displayed in the back office
/// </summary>
[DataContract(Name = "content", Namespace = "")]
public class
ContentItemDisplay<TVariant> : INotificationModel,
IErrorModel // ListViewAwareContentItemDisplayBase<ContentPropertyDisplay, IContent>
public class ContentItemDisplay<TVariant> :
INotificationModel,
IErrorModel // ListViewAwareContentItemDisplayBase<ContentPropertyDisplay, IContent>
where TVariant : ContentVariantDisplay
{
public ContentItemDisplay()

View File

@@ -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<IUmbracoMapper>())
{
_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<ContentItemDisplayWithSchedule, ContentItemDisplay>(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;

View File

@@ -95,16 +95,138 @@ internal class ContentMapDefinition : IMapDefinition
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<IContent, ContentItemBasic<ContentPropertyBasic>>(
(source, context) => new ContentItemBasic<ContentPropertyBasic>(), Map);
mapper.Define<IContent, ContentItemBasic<ContentPropertyBasic>>((source, context) => new ContentItemBasic<ContentPropertyBasic>(), Map);
mapper.Define<IContent, ContentPropertyCollectionDto>((source, context) => new ContentPropertyCollectionDto(), Map);
mapper.Define<IContent, ContentItemDisplay>((source, context) => new ContentItemDisplay(), Map);
mapper.Define<IContent, ContentItemDisplayWithSchedule>(
(source, context) => new ContentItemDisplayWithSchedule(), Map);
mapper.Define<IContent, ContentItemDisplayWithSchedule>((source, context) => new ContentItemDisplayWithSchedule(), Map);
mapper.Define<IContent, ContentVariantDisplay>((source, context) => new ContentVariantDisplay(), Map);
mapper.Define<IContent, ContentVariantScheduleDisplay>((source, context) => new ContentVariantScheduleDisplay(), Map);
mapper.Define<ContentItemDisplayWithSchedule, ContentItemDisplay>((source, context) => new ContentItemDisplay(), Map);
mapper.Define<ContentItemDisplay, ContentItemDisplayWithSchedule>((source, context) => new ContentItemDisplayWithSchedule(), Map);
mapper.Define<ContentVariantDisplay, ContentVariantScheduleDisplay>((source, context) => new ContentVariantScheduleDisplay(), Map);
mapper.Define<ContentVariantScheduleDisplay, ContentVariantDisplay>((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<ContentVariantDisplay, ContentVariantScheduleDisplay>(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<ContentVariantScheduleDisplay> 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

View File

@@ -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<UmbracoTestServerTestBase>(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<UmbracoTestServerTestBase> Factory { get; private set; }
/// <summary>
/// Hook for altering UmbracoBuilder setup
/// </summary>
/// <remarks>
/// Can also be used for registering test doubles.
/// </remarks>
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<AuthenticationSchemeOptions, TestAuthHandler>(TestAuthHandler.TestAuthenticationScheme, options => { }));
});
Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
LinkGenerator = Factory.Services.GetRequiredService<LinkGenerator>();
}
protected HttpClient Client { get; private set; }
protected LinkGenerator LinkGenerator { get; private set; }
protected WebApplicationFactory<UmbracoTestServerTestBase> Factory { get; private set; }
/// <summary>
/// Prepare a url before using <see cref="Client" />.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareApiControllerUrl<T>(Expression<Func<T, object>> methodSelector)
where T : UmbracoApiController
{
var url = LinkGenerator.GetUmbracoApiService(methodSelector);
return PrepareUrl(url);
}
/// <summary>
/// Prepare a url before using <see cref="Client" />.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareSurfaceControllerUrl<T>(Expression<Func<T, object>> methodSelector)
where T : SurfaceController
{
var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector);
return PrepareUrl(url);
}
/// <summary>
/// Prepare a url before using <see cref="Client" />.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareUrl(string url)
{
var umbracoContextFactory = GetRequiredService<IUmbracoContextFactory>();
var httpContextAccessor = GetRequiredService<IHttpContextAccessor>();
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<UmbracoTestServerTestBase>(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<AuthenticationSchemeOptions, TestAuthHandler>(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<LinkGenerator>();
}
/// <summary>
/// Prepare a url before using <see cref="Client"/>.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareApiControllerUrl<T>(Expression<Func<T, object>> methodSelector)
where T : UmbracoApiController
{
var url = LinkGenerator.GetUmbracoApiService(methodSelector);
return PrepareUrl(url);
}
/// <summary>
/// Prepare a url before using <see cref="Client"/>.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareSurfaceControllerUrl<T>(Expression<Func<T, object>> methodSelector)
where T : SurfaceController
{
var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector);
return PrepareUrl(url);
}
/// <summary>
/// Prepare a url before using <see cref="Client"/>.
/// This returns the url but also sets the HttpContext.request into to use this url.
/// </summary>
/// <returns>The string URL of the controller action.</returns>
protected string PrepareUrl(string url)
{
IUmbracoContextFactory umbracoContextFactory = GetRequiredService<IUmbracoContextFactory>();
IHttpContextAccessor httpContextAccessor = GetRequiredService<IHttpContextAccessor>();
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<IRuntime>());
}
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<IRuntime>());
}
});
// 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<T>() => Factory.Services.GetRequiredService<T>();
protected virtual T GetRequiredService<T>() => Factory.Services.GetRequiredService<T>();
protected void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestUmbracoDatabaseFactoryProvider>();
protected void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestUmbracoDatabaseFactoryProvider>();
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();
}
/// <summary>
/// Hook for registering test doubles.
/// </summary>
protected virtual void ConfigureTestServices(IServiceCollection services)
{
}
/// <summary>
/// Hook for registering test doubles.
/// </summary>
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();
});
}
}
}

View File

@@ -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<SendingContentNotification> _handler;
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
builder.AddNotificationHandler<SendingContentNotification, FilterEventHandler>();
}
[TearDown]
public void Reset() => ResetNotifications();
[Test]
public async Task Content_Item_With_Schedule_Raises_SendingContentNotification()
{
IContentTypeService contentTypeService = GetRequiredService<IContentTypeService>();
IContentService contentService = GetRequiredService<IContentService>();
IJsonSerializer serializer = GetRequiredService<IJsonSerializer>();
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<ContentController>(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<ContentItemDisplayWithSchedule>(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<IContentTypeService>();
var contentService = GetRequiredService<IContentService>();
var localizationService = GetRequiredService<ILocalizationService>();
IJsonSerializer serializer = GetRequiredService<IJsonSerializer>();
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<ContentController>(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<ContentItemDisplayWithSchedule>(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<SendingContentNotification>
{
public void Handle(SendingContentNotification notification)
{
_messageCount += 1;
_handler?.Invoke(notification);
}
}
}