using System; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Builders; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping { [TestFixture] public class ScopeEventDispatcherTests { [SetUp] public void Setup() { // remove all handlers first DoThing1 = null; DoThing2 = null; DoThing3 = null; } [TestCase(false, true, true)] [TestCase(false, true, false)] [TestCase(false, false, true)] [TestCase(false, false, false)] [TestCase(true, true, true)] [TestCase(true, true, false)] [TestCase(true, false, true)] [TestCase(true, false, false)] public void EventsHandling(bool passive, bool cancel, bool complete) { var counter1 = 0; var counter2 = 0; DoThing1 += (sender, args) => { counter1++; if (cancel) args.Cancel = true; }; DoThing2 += (sender, args) => { counter2++; }; var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: passive ? new PassiveEventDispatcher() : null)) { var cancelled = scope.Events.DispatchCancelable(DoThing1, this, new SaveEventArgs("test")); if (cancelled == false) scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); if (complete) scope.Complete(); } var expected1 = passive ? 0 : 1; Assert.AreEqual(expected1, counter1); int expected2; if (passive) expected2 = 0; else expected2 = cancel ? 0 : (complete ? 1 : 0); Assert.AreEqual(expected2, counter2); } private ScopeProvider GetScopeProvider(NullLoggerFactory instance) { var fileSystems = new FileSystems( instance, Mock.Of(), Options.Create(new GlobalSettings()), Mock.Of()); var mediaFileManager = new MediaFileManager( Mock.Of(), Mock.Of(), instance.CreateLogger(), Mock.Of(), Mock.Of(), Options.Create(new ContentSettings())); return new ScopeProvider( Mock.Of(), fileSystems, Options.Create(new CoreDebugSettings()), mediaFileManager, Mock.Of>(), instance, Mock.Of(), Mock.Of() ); } [Test] public void QueueEvents() { DoThing1 += OnDoThingFail; DoThing2 += OnDoThingFail; DoThing3 += OnDoThingFail; var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); // events have been queued Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); var knownNames = new[] { "DoThing1", "DoThing2", "DoThing3" }; var knownArgTypes = new[] { typeof(SaveEventArgs), typeof(SaveEventArgs), typeof(SaveEventArgs) }; for (var i = 0; i < events.Length; i++) { Assert.AreEqual(knownNames[i], events[i].EventName); Assert.AreEqual(knownArgTypes[i], events[i].Args.GetType()); } } } [Test] public void SupersededEvents() { DoSaveForContent += OnDoThingFail; DoDeleteForContent += OnDoThingFail; DoForTestArgs += OnDoThingFail; DoForTestArgs2 += OnDoThingFail; var contentType = ContentTypeBuilder.CreateBasicContentType(); var content1 = ContentBuilder.CreateBasicContent(contentType); content1.Id = 123; var content2 = ContentBuilder.CreateBasicContent(contentType); content2.Id = 456; var content3 = ContentBuilder.CreateBasicContent(contentType); content3.Id = 789; var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { //content1 will be filtered from the args scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(new[] { content1, content3 })); scope.Events.Dispatch(DoDeleteForContent, this, new DeleteEventArgs(content1), "DoDeleteForContent"); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); //this entire event will be filtered scope.Events.Dispatch(DoForTestArgs, this, new TestEventArgs(content1)); scope.Events.Dispatch(DoForTestArgs2, this, new TestEventArgs2(content1)); // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); Assert.AreEqual(4, events.Length); Assert.AreEqual(typeof(SaveEventArgs), events[0].Args.GetType()); Assert.AreEqual(1, ((SaveEventArgs)events[0].Args).SavedEntities.Count()); Assert.AreEqual(content3.Id, ((SaveEventArgs)events[0].Args).SavedEntities.First().Id); Assert.AreEqual(typeof(DeleteEventArgs), events[1].Args.GetType()); Assert.AreEqual(content1.Id, ((DeleteEventArgs)events[1].Args).DeletedEntities.First().Id); Assert.AreEqual(typeof(SaveEventArgs), events[2].Args.GetType()); Assert.AreEqual(content2.Id, ((SaveEventArgs)events[2].Args).SavedEntities.First().Id); Assert.AreEqual(typeof(TestEventArgs2), events[3].Args.GetType()); } } [Test] public void SupersededEvents2() { Test_Unpublished += OnDoThingFail; Test_Deleted += OnDoThingFail; var contentService = Mock.Of(); var content = Mock.Of(); var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(Test_Unpublished, contentService, new PublishEventArgs(new[] { content }), "Unpublished"); scope.Events.Dispatch(Test_Deleted, contentService, new DeleteEventArgs(new[] { content }), "Deleted"); // see U4-10764 var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); Assert.AreEqual(2, events.Length); } } /// /// This will test that when we track events that before we Get the events we normalize all of the /// event entities to be the latest one (most current) found amongst the event so that there is /// no 'stale' entities in any of the args /// [Test] public void LatestEntities() { DoSaveForContent += OnDoThingFail; var now = DateTime.Now; var contentType = ContentTypeBuilder.CreateBasicContentType(); var content1 = ContentBuilder.CreateBasicContent(contentType); content1.Id = 123; content1.UpdateDate = now.AddMinutes(1); var content2 = ContentBuilder.CreateBasicContent(contentType); content2.Id = 123; content2.UpdateDate = now.AddMinutes(2); var content3 = ContentBuilder.CreateBasicContent(contentType); content3.Id = 123; content3.UpdateDate = now.AddMinutes(3); var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.All).ToArray(); Assert.AreEqual(3, events.Length); foreach (var t in events) { var args = (SaveEventArgs)t.Args; foreach (var entity in args.SavedEntities) { Assert.AreEqual(content3, entity); Assert.IsTrue(object.ReferenceEquals(content3, entity)); } } } } [Test] public void FirstIn() { DoSaveForContent += OnDoThingFail; var now = DateTime.Now; var contentType = ContentTypeBuilder.CreateBasicContentType(); var content1 = ContentBuilder.CreateBasicContent(contentType); content1.Id = 123; content1.UpdateDate = now.AddMinutes(1); var content2 = ContentBuilder.CreateBasicContent(contentType); content2.Id = 123; content1.UpdateDate = now.AddMinutes(2); var content3 = ContentBuilder.CreateBasicContent(contentType); content3.Id = 123; content1.UpdateDate = now.AddMinutes(3); var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.FirstIn).ToArray(); Assert.AreEqual(1, events.Length); Assert.AreEqual(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First()); Assert.IsTrue(object.ReferenceEquals(content1, ((SaveEventArgs)events[0].Args).SavedEntities.First())); Assert.AreEqual(content1.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); } } [Test] public void LastIn() { DoSaveForContent += OnDoThingFail; var now = DateTime.Now; var contentType = ContentTypeBuilder.CreateBasicContentType(); var content1 = ContentBuilder.CreateBasicContent(contentType); content1.Id = 123; content1.UpdateDate = now.AddMinutes(1); var content2 = ContentBuilder.CreateBasicContent(contentType); content2.Id = 123; content2.UpdateDate = now.AddMinutes(2); var content3 = ContentBuilder.CreateBasicContent(contentType); content3.Id = 123; content3.UpdateDate = now.AddMinutes(3); var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content1)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content2)); scope.Events.Dispatch(DoSaveForContent, this, new SaveEventArgs(content3)); // events have been queued var events = scope.Events.GetEvents(EventDefinitionFilter.LastIn).ToArray(); Assert.AreEqual(1, events.Length); Assert.AreEqual(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First()); Assert.IsTrue(object.ReferenceEquals(content3, ((SaveEventArgs)events[0].Args).SavedEntities.First())); Assert.AreEqual(content3.UpdateDate, ((SaveEventArgs)events[0].Args).SavedEntities.First().UpdateDate); } } [TestCase(true)] [TestCase(false)] public void EventsDispatching_Passive(bool complete) { DoThing1 += OnDoThingFail; DoThing2 += OnDoThingFail; DoThing3 += OnDoThingFail; var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance); using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); // events have been queued Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); if (complete) scope.Complete(); } // no event has been raised (else OnDoThingFail would have failed) } [TestCase(true)] [TestCase(false)] public void EventsDispatching_Scope(bool complete) { var counter = 0; IScope ambientScope = null; IScopeContext ambientContext = null; Guid value = Guid.Empty; var scopeProvider = GetScopeProvider(NullLoggerFactory.Instance) as ScopeProvider; DoThing1 += (sender, args) => { counter++; }; DoThing2 += (sender, args) => { counter++; }; DoThing3 += (sender, args) => { ambientScope = scopeProvider.AmbientScope; ambientContext = scopeProvider.AmbientContext; value = scopeProvider.Context.Enlist("value", Guid.NewGuid, (c, o) => { }); counter++; }; Guid guid; using (var scope = scopeProvider.CreateScope()) { Assert.IsNotNull(scopeProvider.AmbientContext); guid = scopeProvider.Context.Enlist("value", Guid.NewGuid, (c, o) => { }); scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); // events have been queued Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); Assert.AreEqual(0, counter); if (complete) scope.Complete(); } if (complete) { // events have been raised Assert.AreEqual(3, counter); Assert.IsNull(ambientScope); // scope was gone Assert.IsNotNull(ambientContext); // but not context Assert.AreEqual(guid, value); // so we got the same value! } else { // else, no event has been raised Assert.AreEqual(0, counter); } // everything's gone Assert.IsNull(scopeProvider.AmbientScope); Assert.IsNull(scopeProvider.AmbientContext); } private static void OnDoThingFail(object sender, EventArgs eventArgs) { Assert.Fail(); } public static event EventHandler> DoSaveForContent; public static event EventHandler> DoDeleteForContent; public static event EventHandler DoForTestArgs; public static event EventHandler DoForTestArgs2; public static event EventHandler> DoThing1; public static event EventHandler> DoThing2; public static event TypedEventHandler> DoThing3; public static event TypedEventHandler> Test_Unpublished; public static event TypedEventHandler> Test_Deleted; public class TestEventArgs : CancellableObjectEventArgs { public TestEventArgs(object eventObject) : base(eventObject) { } public object MyEventObject { get { return EventObject; } } } [SupersedeEvent(typeof(TestEventArgs))] public class TestEventArgs2 : CancellableObjectEventArgs { public TestEventArgs2(object eventObject) : base(eventObject) { } public object MyEventObject { get { return EventObject; } } } public class PassiveEventDispatcher : QueuingEventDispatcherBase { public PassiveEventDispatcher() : base(false) { } protected override void ScopeExitCompleted() { // do nothing } } } }