diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 2f075032090f..9d0ba6308478 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -13,6 +13,7 @@ public abstract class ContentTypeCompositionBase : ContentTypeBase, IContentType { private List _contentTypeComposition = new(); private List _removedContentTypeKeyTracker = new(); + private bool _hasCompositionBeenRemoved; /// /// Initializes a new instance of the class with the specified parent ID. @@ -123,6 +124,23 @@ IPropertyType AcquireProperty(IPropertyType propertyType) } } + /// + /// A boolean flag indicating if a composition has been removed from this instance. + /// + /// + /// This is currently (specifically) used in order to know that we need to refresh the content cache which + /// needs to occur when a composition has been removed from a content type + /// + [IgnoreDataMember] + internal bool HasCompositionTypeBeenRemoved + { + get => _hasCompositionBeenRemoved; + private set + { + _hasCompositionBeenRemoved = value; + OnPropertyChanged(nameof(HasCompositionTypeBeenRemoved)); + } + } /// public IEnumerable GetOriginalComposedPropertyTypes() => GetRawComposedPropertyTypes(); @@ -212,6 +230,7 @@ public bool RemoveContentType(string alias) _removedContentTypeKeyTracker.AddRange(compositionIdsToRemove); } + HasCompositionTypeBeenRemoved = true; OnPropertyChanged(nameof(ContentTypeComposition)); return _contentTypeComposition.Remove(contentTypeComposition); diff --git a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs index 7b9c0a456aee..5d8bbd02a05f 100644 --- a/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs +++ b/src/Umbraco.Core/Services/Changes/ContentTypeChangeTypes.cs @@ -17,14 +17,29 @@ public enum ContentTypeChangeTypes : byte Create = 1, /// - /// Content type changes impact only the Content type being saved + /// Content type changes directly impact existing content of this content type. /// + /// + /// These changes are "destructive" of nature. They include: + /// - Changing the content type alias. + /// - Removing a property type or a composition. + /// - Changing the alias of a property type (this effectively corresponds to removing a property type). + /// - Changing variance, either at property or content type level. + /// RefreshMain = 2, /// - /// Content type changes impacts the content type being saved and others used that are composed of it + /// Content type changes that do not directly impact existing content of this content type. /// - RefreshOther = 4, // changed, other change + /// + /// These changes are "constructive" of nature, and include all changes not included in + /// - for example: + /// - Adding a property type or a composition. + /// - Rearranging property types or groups. + /// - Changes to name, description, icon etc. + /// - Changes to other content type settings, i.e. allowed child types and version cleanup. + /// + RefreshOther = 4, /// /// Content type was removed diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs index 60d9a4917f47..3ba7211a5618 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs @@ -688,6 +688,18 @@ private async Task UpdatePropertiesAsync( // This ensures we correctly handle property types that may have been filtered out from groups. var existingPropertyTypes = contentType.PropertyTypes.ToList(); + // To ensure correct change tracking, we must explicitly inform the content type of any + // existing properties that have been removed. + var removedPropertyTypeAliases = existingPropertyTypes + .Select(pt => pt.Alias) + .Except(model.Properties.Select(p => p.Alias)) + .ToArray(); + + foreach (var removedPropertyTypeAlias in removedPropertyTypeAliases) + { + contentType.RemovePropertyType(removedPropertyTypeAlias); + } + // handle properties in groups PropertyGroup[] propertyGroups = model.Containers.Select(container => { @@ -781,11 +793,14 @@ private IPropertyType MapProperty( } // get the current property type (if it exists) - IPropertyType propertyType = existingPropertyTypes.FirstOrDefault(pt => pt.Key == property.Key) - ?? new PropertyType(_shortStringHelper, dataType); + IPropertyType propertyType = existingPropertyTypes.FirstOrDefault(pt => pt.Alias == property.Alias) + ?? new PropertyType(_shortStringHelper, dataType) + { + // We are demanding a property type key in the model, so we should probably + // ensure that it's the one that's actually used. + Key = property.Key + }; - // We are demanding a property type key in the model, so we should probably ensure that it's the on that's actually used. - propertyType.Key = property.Key; propertyType.Name = property.Name; propertyType.DataTypeId = dataType.Id; propertyType.DataTypeKey = dataType.Key; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs index a987e9a4f78d..dc5850e8ce75 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Create.cs @@ -1,8 +1,10 @@ using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentTypeEditing; using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; @@ -13,6 +15,10 @@ internal sealed partial class ContentTypeEditingServiceTests [TestCase(true)] public async Task Can_Create_With_All_Basic_Settings(bool isElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test", isElement: isElement); createModel.Description = "This is the Test description"; createModel.Icon = "icon icon-something"; @@ -33,6 +39,8 @@ public async Task Can_Create_With_All_Basic_Settings(bool isElement) Assert.AreEqual("This is the Test description", contentType.Description); Assert.AreEqual("icon icon-something", contentType.Icon); Assert.IsTrue(contentType.AllowedAsRoot); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [TestCase(false, false)] @@ -41,6 +49,10 @@ public async Task Can_Create_With_All_Basic_Settings(bool isElement) [TestCase(true, true)] public async Task Can_Create_With_Variation(bool variesByCulture, bool variesBySegment) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test"); createModel.VariesByCulture = variesByCulture; createModel.VariesBySegment = variesBySegment; @@ -54,12 +66,18 @@ public async Task Can_Create_With_Variation(bool variesByCulture, bool variesByS Assert.AreEqual(variesByCulture, contentType.VariesByCulture()); Assert.AreEqual(variesBySegment, contentType.VariesBySegment()); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [TestCase(true)] [TestCase(false)] public async Task Can_Create_In_A_Folder(bool isElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var containerResult = ContentTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder"); Assert.IsTrue(containerResult.Success); var container = containerResult.Result?.Entity; @@ -74,12 +92,18 @@ public async Task Can_Create_In_A_Folder(bool isElement) Assert.IsNotNull(contentType); Assert.AreEqual(container.Id, contentType.ParentId); Assert.AreEqual(isElement, contentType.IsElement); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [TestCase(false)] [TestCase(true)] public async Task Can_Create_With_Properties_In_A_Container(bool isElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test", isElement: isElement); var container = ContentTypePropertyContainerModel(); createModel.Containers = new[] { container }; @@ -101,12 +125,18 @@ public async Task Can_Create_With_Properties_In_A_Container(bool isElement) Assert.AreEqual("testProperty", contentType.PropertyTypes.First().Alias); Assert.AreEqual("testProperty", contentType.PropertyGroups.First().PropertyTypes!.First().Alias); Assert.IsEmpty(contentType.NoGroupPropertyTypes); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [TestCase(false)] [TestCase(true)] public async Task Can_Create_With_Orphaned_Properties(bool isElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test", isElement: isElement); var propertyType = ContentTypePropertyTypeModel("Test Property", "testProperty"); @@ -125,11 +155,17 @@ public async Task Can_Create_With_Orphaned_Properties(bool isElement) Assert.AreEqual("testProperty", contentType.PropertyTypes.First().Alias); Assert.AreEqual(1, contentType.NoGroupPropertyTypes.Count()); Assert.AreEqual("testProperty", contentType.NoGroupPropertyTypes.First().Alias); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Specify_Key() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var key = new Guid("33C326F6-CB5E-43D6-9730-E946AA5F9C7B"); var createModel = ContentTypeCreateModel(key: key); @@ -143,11 +179,17 @@ public async Task Can_Specify_Key() Assert.IsNotNull(contentType); Assert.AreEqual(key, contentType.Key); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Specify_PropertyType_Key() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyTypeKey = new Guid("82DDEBD8-D2CA-423E-B88D-6890F26152B4"); var propertyTypeContainer = ContentTypePropertyContainerModel(); @@ -169,11 +211,17 @@ public async Task Can_Specify_PropertyType_Key() Assert.IsNotNull(propertyType); Assert.AreEqual(propertyTypeKey, propertyType.Key); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Assign_Allowed_Types() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var allowedOne = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Allowed One", "allowedOne"), Constants.Security.SuperUserKey)).Result; var allowedTwo = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Allowed Two", "allowedTwo"), Constants.Security.SuperUserKey)).Result; Assert.IsNotNull(allowedOne); @@ -197,11 +245,17 @@ public async Task Can_Assign_Allowed_Types() Assert.AreEqual(2, allowedContentTypes.Length); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 0 && c.Alias == allowedOne.Alias)); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 1 && c.Alias == allowedTwo.Alias)); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Assign_History_Cleanup() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test"); createModel.Cleanup = new ContentTypeCleanup { @@ -217,6 +271,8 @@ public async Task Can_Assign_History_Cleanup() Assert.IsTrue(contentType.HistoryCleanup.PreventCleanup); Assert.AreEqual(123, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays); Assert.AreEqual(456, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] @@ -226,6 +282,10 @@ public async Task Can_Assign_History_Cleanup() // Wondering where the last case is? Look at the test below. public async Task Can_Create_Composite(bool compositionIsElement, bool contentTypeIsElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase", @@ -267,11 +327,17 @@ public async Task Can_Create_Composite(bool compositionIsElement, bool contentTy Assert.AreEqual(1, compositionType.CompositionPropertyTypes.Count()); Assert.AreEqual(compositionProperty.Key, compositionType.CompositionPropertyTypes.First().Key); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Create_Property_Container_Structure_Matching_Composition_Container_Structure() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -321,11 +387,17 @@ public async Task Can_Create_Property_Container_Structure_Matching_Composition_C Assert.IsTrue(propertyTypeKeys.Contains(property.Key)); Assert.IsTrue(contentTypeGroup.PropertyTypes?.Contains("myProperty")); Assert.IsFalse(contentTypeGroup.PropertyTypes?.Contains("compositionProperty")); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Property_Container_Aliases_Are_CamelCased_Names() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test"); var tab = ContentTypePropertyContainerModel("My Tab", type: TabContainerType); var group1 = ContentTypePropertyContainerModel("My Group", type: GroupContainerType); @@ -348,11 +420,17 @@ public async Task Property_Container_Aliases_Are_CamelCased_Names() Assert.AreEqual("myTab", contentType.PropertyGroups.First(g => g.Name == "My Tab").Alias); Assert.AreEqual("myTab/myGroup", contentType.PropertyGroups.First(g => g.Name == "My Group").Alias); Assert.AreEqual("anotherGroup", contentType.PropertyGroups.First(g => g.Name == "AnotherGroup").Alias); + + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Element_Types_Must_Not_Be_Composed_By_non_element_type() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + // This is a pretty interesting one, since it actually seems to be broken in the old backoffice, // since the client will always send the isElement flag as false to the GetAvailableCompositeContentTypes endpoint // Even if it's an element type, however if we look at the comment in GetAvailableCompositeContentTypes @@ -366,6 +444,9 @@ public async Task Element_Types_Must_Not_Be_Composed_By_non_element_type() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var createModel = ContentTypeCreateModel( "Content Type Using Composition", compositions: new[] @@ -386,16 +467,26 @@ public async Task Element_Types_Must_Not_Be_Composed_By_non_element_type() Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); Assert.IsNull(result.Result); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task ContentType_Containing_Composition_Cannot_Be_Used_As_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel("CompositionBase"); var baseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); Assert.IsTrue(baseResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, baseResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var composition = ContentTypeCreateModel( "Composition", compositions: new[] @@ -409,6 +500,9 @@ public async Task ContentType_Containing_Composition_Cannot_Be_Used_As_Compositi var compositionResult = await ContentTypeEditingService.CreateAsync(composition, Constants.Security.SuperUserKey); Assert.IsTrue(compositionResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, compositionResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // This is not allowed because the composition also has a composition (compositionBase). var invalidComposition = ContentTypeCreateModel( "Invalid", @@ -429,12 +523,14 @@ public async Task ContentType_Containing_Composition_Cannot_Be_Used_As_Compositi Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, invalidAttempt.Status); Assert.IsNull(invalidAttempt.Result); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Can_Create_Child() { - var parentProperty = ContentTypePropertyTypeModel("Parent Property", "parentProperty"); var parentModel = ContentTypeCreateModel( "Parent", @@ -453,6 +549,10 @@ public async Task Can_Create_Child() }, }; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var childModel = ContentTypeCreateModel( "Child", propertyTypes: new[] { childProperty }, @@ -472,12 +572,18 @@ public async Task Can_Create_Child() Assert.IsTrue(contentType.CompositionPropertyTypes.Any(x => x.Alias == parentProperty.Alias)); Assert.IsTrue(contentType.CompositionPropertyTypes.Any(x => x.Alias == childProperty.Alias)); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, result.Result!.Id, ContentTypeChangeTypes.Create); } // Unlike compositions, it is allowed to inherit on multiple levels [Test] public async Task Can_Create_Grandchild() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var rootProperty = ContentTypePropertyTypeModel("Root property"); ContentTypeCreateModel rootModel = ContentTypeCreateModel( "Root", @@ -486,6 +592,9 @@ public async Task Can_Create_Grandchild() var rootResult = await ContentTypeEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); Assert.IsTrue(rootResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, rootResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var childProperty = ContentTypePropertyTypeModel("Child Property", "childProperty"); var rootKey = rootResult.Result!.Key; Composition[] composition = @@ -504,6 +613,9 @@ public async Task Can_Create_Grandchild() var childResult = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); Assert.IsTrue(childResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, childResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var grandchildProperty = ContentTypePropertyTypeModel("Grandchild Property", "grandchildProperty"); var childKey = childResult.Result!.Key; Composition[] grandchildComposition = @@ -542,17 +654,27 @@ public async Task Can_Create_Grandchild() Assert.IsTrue(grandchild.CompositionPropertyTypes.Any(x => x.Alias == childProperty.Alias)); Assert.IsTrue(grandchild.CompositionPropertyTypes.Any(x => x.Alias == grandchildProperty.Alias)); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, grandchild.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Can_Create_Child_To_Content_Type_With_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!; var parentContentType = (await ContentTypeEditingService.CreateAsync( ContentTypeCreateModel( "Parent", compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]), Constants.Security.SuperUserKey)).Result!; + + AssertContentTypeRefreshPayload(refreshedPayloads, parentContentType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var result = await ContentTypeEditingService.CreateAsync( ContentTypeCreateModel( "Child", @@ -571,16 +693,25 @@ public async Task Can_Create_Child_To_Content_Type_With_Composition() Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); }); + + AssertContentTypeRefreshPayload(refreshedPayloads, childContentType.Id, ContentTypeChangeTypes.Create); } [Test] public async Task Cannot_Be_Both_Parent_And_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel("CompositionBase"); var baseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); Assert.IsTrue(baseResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, baseResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var createModel = ContentTypeCreateModel( compositions: new[] { @@ -600,19 +731,35 @@ public async Task Cannot_Be_Both_Parent_And_Composition() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Have_Multiple_Inheritance() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var parentModel1 = ContentTypeCreateModel("Parent1"); var parentModel2 = ContentTypeCreateModel("Parent2"); - var parentKey1 = (await ContentTypeEditingService.CreateAsync(parentModel1, Constants.Security.SuperUserKey)).Result?.Key; + var parent1 = (await ContentTypeEditingService.CreateAsync(parentModel1, Constants.Security.SuperUserKey)).Result; + var parentKey1 = parent1?.Key; Assert.IsTrue(parentKey1.HasValue); - var parentKey2 = (await ContentTypeEditingService.CreateAsync(parentModel2, Constants.Security.SuperUserKey)).Result?.Key; + + AssertContentTypeRefreshPayload(refreshedPayloads, parent1.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + + var parent2 = (await ContentTypeEditingService.CreateAsync(parentModel2, Constants.Security.SuperUserKey)).Result; + var parentKey2 = parent2?.Key; Assert.IsTrue(parentKey2.HasValue); + AssertContentTypeRefreshPayload(refreshedPayloads, parent2.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var childProperty = ContentTypePropertyTypeModel("Child Property", "childProperty"); Composition[] composition = { @@ -635,11 +782,18 @@ public async Task Cannot_Have_Multiple_Inheritance() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Specify_Duplicate_PropertyType_Alias_From_Compositions() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyTypeAlias = "testproperty"; var compositionPropertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); var compositionBase = ContentTypeCreateModel( @@ -649,6 +803,9 @@ public async Task Cannot_Specify_Duplicate_PropertyType_Alias_From_Compositions( var compositionBaseResult = await ContentTypeEditingService.CreateAsync(compositionBase, Constants.Security.SuperUserKey); Assert.IsTrue(compositionBaseResult.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, compositionBaseResult.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var createModel = ContentTypeCreateModel( compositions: new[] { @@ -667,11 +824,18 @@ public async Task Cannot_Specify_Duplicate_PropertyType_Alias_From_Compositions( Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicatePropertyTypeAlias, result.Status); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Specify_Non_Existent_DocType_As_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel( compositions: new[] { @@ -687,15 +851,26 @@ public async Task Cannot_Specify_Non_Existent_DocType_As_Composition() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Mix_Inheritance_And_ParentKey() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var parentModel = ContentTypeCreateModel("Parent"); - var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result; + var parentKey = parent?.Key; Assert.IsTrue(parentKey.HasValue); + AssertContentTypeRefreshPayload(refreshedPayloads, parent.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var containerResult = ContentTypeService.CreateContainer(Constants.System.Root, Guid.NewGuid(), "Test folder"); Assert.IsTrue(containerResult.Success); var container = containerResult.Result?.Entity; @@ -718,15 +893,25 @@ public async Task Cannot_Mix_Inheritance_And_ParentKey() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Have_Same_Key_For_Inheritance_And_Parent() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var parentModel = ContentTypeCreateModel("Parent"); var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result; Assert.IsNotNull(parent); + AssertContentTypeRefreshPayload(refreshedPayloads, parent.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + Composition[] composition = { new() @@ -744,15 +929,26 @@ public async Task Cannot_Have_Same_Key_For_Inheritance_And_Parent() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Use_As_ParentKey() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var parentModel = ContentTypeCreateModel("Parent"); - var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result; + var parentKey = parent?.Key; Assert.IsTrue(parentKey.HasValue); + AssertContentTypeRefreshPayload(refreshedPayloads, parent.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + var childModel = ContentTypeCreateModel( "Child", containerKey: parentKey.Value); @@ -761,6 +957,9 @@ public async Task Cannot_Use_As_ParentKey() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("")] @@ -771,12 +970,19 @@ public async Task Cannot_Use_As_ParentKey() [TestCase("!\"#ยค%&/()=)?`")] public async Task Cannot_Use_Invalid_PropertyType_Alias(string propertyTypeAlias) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType }); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("testProperty", "testProperty")] @@ -785,6 +991,10 @@ public async Task Cannot_Use_Invalid_PropertyType_Alias(string propertyTypeAlias [TestCase("testProperty", "testproperty")] public async Task Cannot_Use_Duplicate_PropertyType_Alias(string propertyTypeAlias1, string propertyTypeAlias2) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyType1 = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias1); var propertyType2 = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias2); var createModel = ContentTypeCreateModel("Test", propertyTypes: new[] { propertyType1, propertyType2 }); @@ -792,6 +1002,9 @@ public async Task Cannot_Use_Duplicate_PropertyType_Alias(string propertyTypeAli var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicatePropertyTypeAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("testAlias", "testAlias")] @@ -802,39 +1015,64 @@ public async Task Cannot_Use_Duplicate_PropertyType_Alias(string propertyTypeAli [TestCase("TESTALIAS", "testAlias")] public async Task Cannot_Use_Alias_As_PropertyType_Alias(string contentTypeAlias, string propertyTypeAlias) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyType = ContentTypePropertyTypeModel("Test Property", propertyTypeAlias); var createModel = ContentTypeCreateModel("Test", contentTypeAlias, propertyTypes: new[] { propertyType }); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.PropertyTypeAliasCannotEqualContentTypeAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Use_Non_Existing_DataType_For_PropertyType() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyType = ContentTypePropertyTypeModel("Test Property", "testProperty", dataTypeKey: Guid.NewGuid()); var createModel = ContentTypeCreateModel("Test", "test", propertyTypes: new[] { propertyType }); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DataTypeNotFound, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Use_Empty_Alias_For_PropertyType() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var propertyType = ContentTypePropertyTypeModel("Test Property", string.Empty); var createModel = ContentTypeCreateModel("Test", "test", propertyTypes: new[] { propertyType }); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidPropertyTypeAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Use_Empty_Name_For_PropertyType_Container() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var container = ContentTypePropertyContainerModel(string.Empty); var propertyType = ContentTypePropertyTypeModel("Test Property", "testProperty", containerKey: container.Key); var createModel = ContentTypeCreateModel("Test", "test", propertyTypes: new[] { propertyType }); @@ -843,6 +1081,9 @@ public async Task Cannot_Use_Empty_Name_For_PropertyType_Container() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerName, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("")] @@ -854,30 +1095,51 @@ public async Task Cannot_Use_Empty_Name_For_PropertyType_Container() [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { "System" })] public async Task Cannot_Use_Invalid_Alias(string contentTypeAlias) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", contentTypeAlias); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("test")] // Matches alias case sensitively. [TestCase("Test")] // Matches alias case insensitively. public async Task Cannot_Use_Existing_Alias(string newAlias) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test"); var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); + AssertContentTypeRefreshPayload(refreshedPayloads, result.Result!.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + createModel = ContentTypeCreateModel("Test 2", newAlias); result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicateAlias, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Add_Container_From_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -893,6 +1155,9 @@ public async Task Cannot_Add_Container_From_Composition() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // Create doc type using the composition var createModel = ContentTypeCreateModel( compositions: new[] @@ -908,11 +1173,18 @@ public async Task Cannot_Add_Container_From_Composition() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Duplicate_Container_Key_From_Composition() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -927,6 +1199,9 @@ public async Task Cannot_Duplicate_Container_Key_From_Composition() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // Create doc type using the composition var createModel = ContentTypeCreateModel( compositions: new[] @@ -943,11 +1218,18 @@ public async Task Cannot_Duplicate_Container_Key_From_Composition() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Have_Duplicate_Container_Key() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + // Create doc type using the composition var createModel = ContentTypeCreateModel("Test", "test"); @@ -960,11 +1242,18 @@ public async Task Cannot_Have_Duplicate_Container_Key() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicateContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Add_Property_To_Missing_Container() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -979,6 +1268,9 @@ public async Task Cannot_Add_Property_To_Missing_Container() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // Create doc type using the composition var createModel = ContentTypeCreateModel( compositions: new[] @@ -993,11 +1285,18 @@ public async Task Cannot_Add_Property_To_Missing_Container() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Add_Property_Container_To_Missing_Container() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + // Create doc type using the composition var createModel = ContentTypeCreateModel(); @@ -1012,11 +1311,18 @@ public async Task Cannot_Add_Property_Container_To_Missing_Container() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Create_Property_In_Composition_Container() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -1032,6 +1338,9 @@ public async Task Cannot_Create_Property_In_Composition_Container() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // Create doc type using the composition var createModel = ContentTypeCreateModel( compositions: new[] @@ -1046,11 +1355,18 @@ public async Task Cannot_Create_Property_In_Composition_Container() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Create_Property_Container_In_Composition_Container() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = ContentTypeCreateModel( "Composition Base", "compositionBase"); @@ -1066,6 +1382,9 @@ public async Task Cannot_Create_Property_Container_In_Composition_Container() Assert.IsTrue(compositionResult.Success); var compositionType = compositionResult.Result; + AssertContentTypeRefreshPayload(refreshedPayloads, compositionType.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + // Create doc type using the composition var createModel = ContentTypeCreateModel( compositions: new[] @@ -1083,11 +1402,18 @@ public async Task Cannot_Create_Property_Container_In_Composition_Container() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] public async Task Cannot_Create_Composite_With_MediaType() { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var compositionBase = MediaTypeCreateModel("Composition Base"); // Let's add a property to ensure that it passes through @@ -1111,6 +1437,9 @@ public async Task Cannot_Create_Composite_With_MediaType() var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); + + // no changes should have been notified (media type creation succeeds, but we are listening for content type change notifications) + Assert.IsNull(refreshedPayloads); } [TestCase("something")] @@ -1118,6 +1447,10 @@ public async Task Cannot_Create_Composite_With_MediaType() [TestCase("group")] public async Task Cannot_Create_Container_With_Unknown_Type(string containerType) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var createModel = ContentTypeCreateModel("Test", "test"); var container = ContentTypePropertyContainerModel(name: containerType, type: containerType); createModel.Containers = new[] { container }; @@ -1128,17 +1461,28 @@ public async Task Cannot_Create_Container_With_Unknown_Type(string containerType var result = await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerType, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase(false, true)] [TestCase(true, false)] public async Task Cannot_Have_Element_Type_Mismatched_Inheritance(bool parentIsElement, bool childIsElement) { + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var parentModel = ContentTypeCreateModel("Parent1", isElement: parentIsElement); - var parentKey = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result?.Key; + var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result; + var parentKey = parent?.Key; Assert.IsTrue(parentKey.HasValue); + AssertContentTypeRefreshPayload(refreshedPayloads, parent.Id, ContentTypeChangeTypes.Create); + refreshedPayloads = null; + Composition[] composition = { new() @@ -1156,5 +1500,8 @@ public async Task Cannot_Have_Element_Type_Mismatched_Inheritance(bool parentIsE Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidElementFlagComparedToParent, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs index 36884242b4f8..ae7998d603d1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs @@ -1,7 +1,9 @@ using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Tests.Common.Builders; @@ -19,7 +21,11 @@ public async Task Can_Update_All_Basic_Settings(bool isElement) createModel.AllowedAsRoot = true; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; - var updateModel = ContentTypeUpdateModel("Test updated", "testUpdated", isElement: isElement); + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Test updated", "test", isElement: isElement); updateModel.Description = "This is the Test description updated"; updateModel.Icon = "icon icon-something-updated"; updateModel.AllowedAsRoot = false; @@ -32,13 +38,42 @@ public async Task Can_Update_All_Basic_Settings(bool isElement) Assert.IsNotNull(contentType); Assert.AreEqual(isElement, contentType.IsElement); - Assert.AreEqual("testUpdated", contentType.Alias); + Assert.AreEqual("test", contentType.Alias); Assert.AreEqual("Test updated", contentType.Name); Assert.AreEqual(result.Result.Id, contentType.Id); Assert.AreEqual(result.Result.Key, contentType.Key); Assert.AreEqual("This is the Test description updated", contentType.Description); Assert.AreEqual("icon icon-something-updated", contentType.Icon); Assert.IsFalse(contentType.AllowedAsRoot); + + // expect RefreshOther when changing basic settings only + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Update_Alias(bool isElement) + { + var createModel = ContentTypeCreateModel("Test", "test", isElement: isElement); + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Test updated", "testUpdated", isElement: isElement); + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual("testUpdated", contentType.Alias); + + // expect RefreshMain when changing alias + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); } [TestCase(false, false)] @@ -53,6 +88,10 @@ public async Task Can_Update_Variation(bool variesByCulture, bool variesBySegmen var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.VariesByCulture = !variesByCulture; updateModel.VariesBySegment = !variesBySegment; @@ -66,6 +105,76 @@ public async Task Can_Update_Variation(bool variesByCulture, bool variesBySegmen Assert.AreEqual(!variesByCulture, contentType.VariesByCulture()); Assert.AreEqual(!variesBySegment, contentType.VariesBySegment()); + + // expect RefreshMain when changing variation at content type level + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); + } + + [TestCase(false, false)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(true, true)] + public async Task Can_Update_Property_Variation(bool variesByCulture, bool variesBySegment) + { + var createModel = ContentTypeCreateModel("Test", "test"); + + var container = ContentTypePropertyContainerModel(); + createModel.Containers = [container]; + + var propertyTypeModel = ContentTypePropertyTypeModel("Test Property", "testProperty", containerKey: container.Key); + createModel.Properties = [propertyTypeModel]; + + createModel.VariesByCulture = variesByCulture; + createModel.VariesBySegment = variesBySegment; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + var propertyType = contentType.PropertyTypes.Single(); + Assert.Multiple(() => + { + Assert.AreEqual("testProperty", propertyType.Alias); + Assert.IsTrue(propertyType.VariesByNothing()); + }); + + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Test", "test"); + updateModel.VariesByCulture = variesByCulture; + updateModel.VariesBySegment = variesBySegment; + + updateModel.Containers = [container]; + + propertyTypeModel = ContentTypePropertyTypeModel("Test Property", "testProperty", containerKey: container.Key); + propertyTypeModel.VariesByCulture = variesByCulture; + propertyTypeModel.VariesBySegment = variesBySegment; + updateModel.Properties = [propertyTypeModel]; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + Assert.IsNotNull(contentType); + + propertyType = contentType.PropertyTypes.Single(); + Assert.Multiple(() => + { + Assert.AreEqual("testProperty", propertyType.Alias); + Assert.AreEqual(variesByCulture, propertyType.VariesByCulture()); + Assert.AreEqual(variesBySegment, propertyType.VariesBySegment()); + }); + + if (variesByCulture || variesBySegment) + { + // expect RefreshMain when changing variation at property type level + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); + } + else + { + // expect RefreshOther when not property level variation (in effect, no real changes were made here) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); + } } [Test] @@ -81,6 +190,10 @@ public async Task Can_Add_Allowed_Types() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.AllowedContentTypes = new[] { @@ -100,6 +213,9 @@ public async Task Can_Add_Allowed_Types() Assert.AreEqual(2, allowedContentTypes.Length); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 0 && c.Alias == allowedOne.Alias)); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 1 && c.Alias == allowedTwo.Alias)); + + // expect RefreshOther when changing allowed types + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -116,6 +232,10 @@ public async Task Can_Remove_Allowed_Types() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.AllowedContentTypes = Array.Empty(); @@ -129,6 +249,9 @@ public async Task Can_Remove_Allowed_Types() var allowedContentTypes = contentType.AllowedContentTypes?.ToArray(); Assert.IsNotNull(allowedContentTypes); Assert.AreEqual(0, allowedContentTypes.Length); + + // expect RefreshOther when changing allowed types + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -145,6 +268,10 @@ public async Task Can_Rearrange_Allowed_Types() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.AllowedContentTypes = new[] { @@ -164,6 +291,9 @@ public async Task Can_Rearrange_Allowed_Types() Assert.AreEqual(2, allowedContentTypes.Length); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedOne.Key && c.SortOrder == 1 && c.Alias == allowedOne.Alias)); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == allowedTwo.Key && c.SortOrder == 0 && c.Alias == allowedTwo.Alias)); + + // expect RefreshOther when changing allowed types + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -173,6 +303,10 @@ public async Task Can_Add_Self_To_Allowed_Types() var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; var id = contentType.Id; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.AllowedContentTypes = new[] { @@ -190,6 +324,9 @@ public async Task Can_Add_Self_To_Allowed_Types() Assert.IsNotNull(allowedContentTypes); Assert.AreEqual(1, allowedContentTypes.Length); Assert.IsTrue(allowedContentTypes.Any(c => c.Key == contentType.Key && c.SortOrder == 0 && c.Alias == contentType.Alias)); + + // expect RefreshOther when changing allowed types + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [TestCase(false)] @@ -205,6 +342,10 @@ public async Task Can_Add_Properties(bool isElement) var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); updateModel.Containers = new[] { container }; var newPropertyType = ContentTypePropertyTypeModel("Test Property 2", "testProperty2", containerKey: container.Key); @@ -234,6 +375,9 @@ public async Task Can_Add_Properties(bool isElement) Assert.AreEqual("testProperty", propertyTypesInContainer.Last().Alias); Assert.IsEmpty(contentType.NoGroupPropertyTypes); + + // expect RefreshOther when adding properties + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [TestCase(false)] @@ -249,6 +393,10 @@ public async Task Can_Remove_Properties(bool isElement) var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); updateModel.Containers = new[] { container }; updateModel.Properties = Array.Empty(); @@ -266,6 +414,57 @@ public async Task Can_Remove_Properties(bool isElement) Assert.AreEqual(0, contentType.PropertyTypes.Count()); Assert.AreEqual(0, contentType.NoGroupPropertyTypes.Count()); + + // expect RefreshMain when removing properties + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); + } + + [TestCase(false)] + [TestCase(true)] + public async Task Can_Remove_Single_Property_From_Container(bool isElement) + { + var createModel = ContentTypeCreateModel("Test", "test", isElement: isElement); + var container = ContentTypePropertyContainerModel(); + createModel.Containers = [container]; + + createModel.Properties = + [ + ContentTypePropertyTypeModel("Test Property", "testProperty", containerKey: container.Key), + ContentTypePropertyTypeModel("Test Property 2", "testProperty2", containerKey: container.Key), + ]; + + var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + Assert.AreEqual(1, contentType.PropertyGroups.Count); + Assert.AreEqual(2, contentType.PropertyTypes.Count()); + + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); + updateModel.Containers = [container]; + updateModel.Properties = + [ + ContentTypePropertyTypeModel("Test Property 2", "testProperty2", containerKey: container.Key), + ]; + + var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + + // Ensure it's actually persisted + contentType = await ContentTypeService.GetAsync(result.Result!.Key); + + Assert.IsNotNull(contentType); + Assert.AreEqual(isElement, contentType.IsElement); + Assert.AreEqual(1, contentType.PropertyGroups.Count); + Assert.AreEqual(1, contentType.PropertyTypes.Count()); + Assert.AreEqual("testProperty2", contentType.PropertyTypes.Single().Alias); + + Assert.AreEqual(0, contentType.NoGroupPropertyTypes.Count()); + + // expect RefreshMain when removing properties + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); } [Test] @@ -286,6 +485,10 @@ public async Task Can_Remove_Properties_Without_Container() Assert.AreEqual("testProperty", contentType.NoGroupPropertyTypes.Single().Alias); Assert.AreEqual(0, contentType.PropertyGroups.Count); + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + // Update the content type removing the property. var updateModel = ContentTypeUpdateModel("Test", "test", isElement: true); updateModel.Properties = Array.Empty(); @@ -302,6 +505,9 @@ public async Task Can_Remove_Properties_Without_Container() Assert.AreEqual(0, contentType.PropertyGroups.Count); Assert.AreEqual(0, contentType.PropertyTypes.Count()); Assert.AreEqual(0, contentType.NoGroupPropertyTypes.Count()); + + // expect RefreshMain when removing properties + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); } [TestCase(false)] @@ -316,6 +522,10 @@ public async Task Can_Edit_Properties(bool isElement) var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; var originalPropertyTypeKey = contentType.PropertyTypes.First().Key; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); propertyType = ContentTypePropertyTypeModel("Test Property 2", "testProperty", key: originalPropertyTypeKey); propertyType.Description = "The updated description"; @@ -340,6 +550,9 @@ public async Task Can_Edit_Properties(bool isElement) Assert.AreEqual(originalPropertyTypeKey, property.Key); Assert.AreEqual(1, contentType.NoGroupPropertyTypes.Count()); + + // expect RefreshOther when changing basic property info (not alias and not variance) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } public enum PropertyMoveOperation @@ -411,6 +624,10 @@ public async Task Can_Move_Properties_To_Another_Container(string containerType, propertyType1.ContainerKey = container2.Key; } + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Test Content Type", containers: [container1, container2], @@ -454,6 +671,9 @@ public async Task Can_Move_Properties_To_Another_Container(string containerType, Assert.AreEqual(property.Name, updatedContent?.Properties[property.Alias]?.GetValue()); } }); + + // expect RefreshOther when changing moving properties around internally on the content type + AssertContentTypeRefreshPayload(refreshedPayloads, createAttempt.Result.Id, ContentTypeChangeTypes.RefreshOther); } [TestCase(false)] @@ -473,6 +693,10 @@ public async Task Can_Rearrange_Containers(bool isElement) var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); container2.SortOrder = 0; container1.SortOrder = 1; @@ -493,6 +717,9 @@ public async Task Can_Rearrange_Containers(bool isElement) var sortedPropertyGroups = contentType.PropertyGroups.OrderBy(g => g.SortOrder).ToArray(); Assert.AreEqual("testProperty2", sortedPropertyGroups.First().PropertyTypes!.Single().Alias); Assert.AreEqual("testProperty1", sortedPropertyGroups.Last().PropertyTypes!.Single().Alias); + + // expect RefreshOther when changing moving properties (containers) around internally on the content type + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [TestCase(false)] @@ -510,6 +737,10 @@ public async Task Can_Make_Properties_Orphaned(bool isElement) var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); updateModel.Containers = [container1]; propertyType2.ContainerKey = null; @@ -531,6 +762,9 @@ public async Task Can_Make_Properties_Orphaned(bool isElement) Assert.AreEqual("testProperty1", contentType.PropertyGroups.First().PropertyTypes!.Single().Alias); Assert.AreEqual("testProperty2", contentType.NoGroupPropertyTypes.Single().Alias); + + // expect RefreshOther when changing moving properties (containers) around internally on the content type + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -547,6 +781,10 @@ public async Task Can_Add_Compositions() createModel.Properties = new[] { propertyType2 }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Properties = new[] { propertyType2 }; updateModel.Compositions = new[] @@ -568,6 +806,9 @@ public async Task Can_Add_Compositions() Assert.AreEqual(2, propertyTypeAliases.Length); Assert.IsTrue(propertyTypeAliases.Contains("testProperty1")); Assert.IsTrue(propertyTypeAliases.Contains("testProperty2")); + + // expect RefreshOther when adding compositions (corresponds to adding properties) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -588,6 +829,10 @@ public async Task Can_Reapply_Compositions() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Properties = new[] { propertyType2 }; updateModel.Compositions = new[] @@ -609,6 +854,9 @@ public async Task Can_Reapply_Compositions() Assert.AreEqual(2, propertyTypeAliases.Length); Assert.IsTrue(propertyTypeAliases.Contains("testProperty1")); Assert.IsTrue(propertyTypeAliases.Contains("testProperty2")); + + // expect RefreshOther when adding compositions (corresponds to adding properties) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -629,6 +877,10 @@ public async Task Can_Remove_Compositions() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Properties = new[] { propertyType2 }; updateModel.Compositions = Array.Empty(); @@ -644,6 +896,9 @@ public async Task Can_Remove_Compositions() Assert.IsEmpty(contentType.ContentTypeComposition); Assert.AreEqual(1, contentType.CompositionPropertyTypes.Count()); Assert.AreEqual("testProperty2", contentType.CompositionPropertyTypes.Single().Alias); + + // expect RefreshMain when removing compositions (corresponds to removing properties) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshMain); } [Test] @@ -661,6 +916,10 @@ public async Task Can_Reapply_Inheritance() var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; var originalPath = contentType.Path; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Child", compositions: new Composition[] @@ -679,6 +938,9 @@ public async Task Can_Reapply_Inheritance() Assert.AreEqual(parentContentType.Id, contentType.ParentId); Assert.AreEqual(originalPath, contentType.Path); Assert.AreEqual($"-1,{parentContentType.Id},{contentType.Id}", contentType.Path); + + // expect RefreshOther when re-applying inheritance (in principle, nothing changes) + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -691,7 +953,11 @@ public async Task Can_Update_History_Cleanup() }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; - var updateModel = ContentTypeUpdateModel("Test updated", "testUpdated"); + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Test updated", "test"); updateModel.Cleanup = new ContentTypeCleanup { PreventCleanup = false, KeepAllVersionsNewerThanDays = 234, KeepLatestVersionPerDayForDays = 567 @@ -708,6 +974,9 @@ public async Task Can_Update_History_Cleanup() Assert.IsFalse(contentType.HistoryCleanup.PreventCleanup); Assert.AreEqual(234, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays); Assert.AreEqual(567, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); + + // expect RefreshOther when changing basic settings + AssertContentTypeRefreshPayload(refreshedPayloads, contentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -725,8 +994,13 @@ public async Task Can_Reapply_Compositions_For_Content_Type_With_Children() compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Parent Updated", + alias: parentContentType.Alias, compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]); var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey); @@ -752,6 +1026,9 @@ public async Task Can_Reapply_Compositions_For_Content_Type_With_Children() Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); }); + + // expect RefreshOther when re-applying compositions (in principle, nothing changes) + AssertContentTypeRefreshPayload(refreshedPayloads, parentContentType.Id, ContentTypeChangeTypes.RefreshOther); } [Test] @@ -769,7 +1046,11 @@ public async Task Can_Remove_Compositions_For_Content_Type_With_Children() compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), Constants.Security.SuperUserKey)).Result!; - var updateModel = ContentTypeUpdateModel("Parent Updated", compositions: []); + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + + var updateModel = ContentTypeUpdateModel("Parent Updated", alias: parentContentType.Alias, compositions: []); var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); @@ -793,6 +1074,13 @@ public async Task Can_Remove_Compositions_For_Content_Type_With_Children() Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); }); + + // expect RefreshMain when removing compositions (corresponds to removing properties) + // - note that both parent and child content type are affected here + Assert.IsNotNull(refreshedPayloads); + Assert.AreEqual(2, refreshedPayloads.Length); + AssertContentTypeRefreshPayload([refreshedPayloads.First()], parentContentType.Id, ContentTypeChangeTypes.RefreshMain); + AssertContentTypeRefreshPayload([refreshedPayloads.Last()], childContentType.Id, ContentTypeChangeTypes.RefreshMain); } [TestCase(false)] @@ -808,6 +1096,10 @@ public async Task Cannot_Move_Properties_To_Non_Existing_Containers(bool isEleme var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); property.ContainerKey = Guid.NewGuid(); updateModel.Containers = new[] { container }; @@ -816,6 +1108,9 @@ public async Task Cannot_Move_Properties_To_Non_Existing_Containers(bool isEleme var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase(false)] @@ -831,6 +1126,10 @@ public async Task Cannot_Move_Containers_To_Non_Existing_Containers(bool isEleme var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test", isElement: isElement); container.ParentKey = Guid.NewGuid(); updateModel.Containers = new[] { container }; @@ -839,6 +1138,9 @@ public async Task Cannot_Move_Containers_To_Non_Existing_Containers(bool isEleme var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.MissingContainer, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -849,6 +1151,10 @@ public async Task Cannot_Add_Self_As_Composition() createModel.Properties = new[] { property }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Properties = new[] { property }; updateModel.Compositions = new[] @@ -859,6 +1165,9 @@ public async Task Cannot_Add_Self_As_Composition() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidComposition, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -876,6 +1185,10 @@ public async Task Cannot_Change_Inheritance() var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Child", compositions: new Composition[] @@ -886,6 +1199,9 @@ public async Task Cannot_Change_Inheritance() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -894,6 +1210,10 @@ public async Task Cannot_Add_Inheritance() var parentContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; var contentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Child"), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Child", compositions: new Composition[] @@ -904,6 +1224,9 @@ public async Task Cannot_Add_Inheritance() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -921,6 +1244,10 @@ public async Task Cannot_Add_Multiple_Inheritance() var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Child", compositions: new Composition[] @@ -932,6 +1259,9 @@ public async Task Cannot_Add_Multiple_Inheritance() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -941,6 +1271,10 @@ public async Task Cannot_Add_Self_As_Inheritance() var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Compositions = new Composition[] { @@ -950,6 +1284,9 @@ public async Task Cannot_Add_Self_As_Inheritance() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -960,6 +1297,10 @@ public async Task Cannot_Add_Inheritance_When_Created_In_A_Folder() var parentContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; var contentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Child", containerKey: container.Key), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Child", compositions: new Composition[] @@ -970,6 +1311,9 @@ public async Task Cannot_Add_Inheritance_When_Created_In_A_Folder() var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase(CompositionType.Composition, CompositionType.Inheritance)] @@ -986,6 +1330,10 @@ public async Task Cannot_Change_Composition_To_Inheritance(CompositionType from, }; var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); updateModel.Compositions = new[] { @@ -995,6 +1343,9 @@ public async Task Cannot_Change_Composition_To_Inheritance(CompositionType from, var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidInheritance, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [TestCase("something")] @@ -1011,6 +1362,10 @@ public async Task Cannot_Update_Container_Types_To_Unknown_Types(string containe var contentType = (await ContentTypeEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel("Test", "test"); container.Type = containerType; updateModel.Containers = new[] { container }; @@ -1019,6 +1374,9 @@ public async Task Cannot_Update_Container_Types_To_Unknown_Types(string containe var result = await ContentTypeEditingService.UpdateAsync(contentType, updateModel, Constants.Security.SuperUserKey); Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerType, result.Status); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } @@ -1033,6 +1391,10 @@ public async Task Cannot_Add_Compositions_For_Content_Type_With_Children() compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Parent Updated", compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]); @@ -1059,6 +1421,9 @@ public async Task Cannot_Add_Compositions_For_Content_Type_With_Children() Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -1077,6 +1442,10 @@ public async Task Cannot_Add_Composition_With_Conflicting_Property_Type_Alias() propertyTypes: [ContentTypePropertyTypeModel("Same Test Property Alias", "testProperty")]), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[] refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Target", propertyTypes: [targetContentTypePropertyType], @@ -1089,6 +1458,9 @@ public async Task Cannot_Add_Composition_With_Conflicting_Property_Type_Alias() Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.DuplicatePropertyTypeAlias, result.Status); }); + + // no changes should have been notified + Assert.IsNull(refreshedPayloads); } [Test] @@ -1106,6 +1478,10 @@ public async Task Can_Add_Composition_With_Conflicting_Property_Type_Alias_When_ propertyTypes: [ContentTypePropertyTypeModel("Same Test Property Alias", "testProperty")]), Constants.Security.SuperUserKey)).Result!; + ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads = null; + ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = payloads + => refreshedPayloads = payloads; + var updateModel = ContentTypeUpdateModel( "Target", propertyTypes: [], @@ -1135,5 +1511,21 @@ public async Task Can_Add_Composition_With_Conflicting_Property_Type_Alias_When_ Assert.AreEqual("testProperty", compositionProperty.Alias); Assert.AreEqual("Same Test Property Alias", compositionProperty.Name); }); + + // expect RefreshMain, because a property was removed to "make room" for the new compositions + AssertContentTypeRefreshPayload(refreshedPayloads, targetContentType.Id, ContentTypeChangeTypes.RefreshMain); + } + + private static void AssertContentTypeRefreshPayload(ContentTypeCacheRefresher.JsonPayload[]? refreshedPayloads, int expectedContentTypeId, ContentTypeChangeTypes expectedChangeTypes) + { + Assert.IsNotNull(refreshedPayloads); + Assert.AreEqual(1, refreshedPayloads.Length); + Assert.Multiple(() => + { + var payload = refreshedPayloads.First(); + Assert.AreEqual(expectedContentTypeId, payload.Id); + Assert.AreEqual(expectedChangeTypes, payload.ChangeTypes); + Assert.AreEqual(nameof(IContentType), payload.ItemType); + }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs index 628073d6e3df..b4e1668ad91f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.cs @@ -1,5 +1,38 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; internal sealed partial class ContentTypeEditingServiceTests : ContentTypeEditingServiceTestsBase { + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.Services.AddUnique(); + base.CustomTestSetup(builder); + } + + [SetUp] + public void SetupTest() => ContentTypeCacheRefreshedNotificationHandler.ContentTypeCacheRefreshed = null; + + private class ContentTypeCacheRefreshedNotificationHandler : INotificationHandler + { + public static Action? ContentTypeCacheRefreshed { get; set; } + + public void Handle(ContentTypeCacheRefresherNotification notification) + { + if (notification.MessageType != MessageType.RefreshByPayload || notification.MessageObject is not ContentTypeCacheRefresher.JsonPayload[] payloads) + { + throw new NotSupportedException(); + } + + ContentTypeCacheRefreshed?.Invoke(payloads); + } + } + }