diff --git a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs index 03ea9ef..49d4fd3 100644 --- a/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs +++ b/src/Umbraco.Cms.Search.Core/NotificationHandlers/RebuildIndexesNotificationHandler.cs @@ -3,12 +3,19 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Search.Core.Configuration; +using Umbraco.Cms.Search.Core.Models.Configuration; using Umbraco.Cms.Search.Core.Services.ContentIndexing; namespace Umbraco.Cms.Search.Core.NotificationHandlers; -internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, INotificationHandler +internal sealed class RebuildIndexesNotificationHandler : INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler + { private readonly IContentIndexingService _contentIndexingService; private readonly ILogger _logger; @@ -37,7 +44,7 @@ public void Handle(LanguageDeletedNotification notification) { _logger.LogInformation("Rebuilding search indexes after language deletion..."); - foreach (var indexRegistration in _options.GetIndexRegistrations()) + foreach (IndexRegistration indexRegistration in _options.GetIndexRegistrations()) { if (indexRegistration.ContainedObjectTypes.Contains(UmbracoObjectTypes.Document)) { @@ -45,4 +52,33 @@ public void Handle(LanguageDeletedNotification notification) } } } + + public void Handle(ContentTypeChangedNotification notification) + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Document); + + public void Handle(MemberTypeChangedNotification notification) + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Member); + + public void Handle(MediaTypeChangedNotification notification) + => RebuildByObjectType(notification.Changes, UmbracoObjectTypes.Media); + + private void RebuildByObjectType(IEnumerable> changes, UmbracoObjectTypes objectType) + where T : class, IContentTypeComposition + { + foreach (ContentTypeChange change in changes) + { + if (change.ChangeTypes is not (ContentTypeChangeTypes.RefreshMain or ContentTypeChangeTypes.Remove)) + { + continue; + } + + foreach (IndexRegistration indexRegistration in _options.GetIndexRegistrations()) + { + if (indexRegistration.ContainedObjectTypes.Contains(objectType)) + { + _contentIndexingService.Rebuild(indexRegistration.IndexAlias); + } + } + } + } } diff --git a/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs new file mode 100644 index 0000000..c999d6f --- /dev/null +++ b/src/Umbraco.Test.Search.Examine.Integration/Tests/ContentTests/SearchService/DocumentTypeTests.cs @@ -0,0 +1,138 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Search.Core.Models.Searching; +using Umbraco.Cms.Search.Core.NotificationHandlers; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.Common.Testing; + +namespace Umbraco.Test.Search.Examine.Integration.Tests.ContentTests.SearchService; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class DocumentTypeTests : SearcherTestBase +{ + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentType _parentContentType = null!; + private IContentType _childContentType = null!; + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + builder.AddNotificationHandler(); + } + + [Test] + public async Task CannotSearchForRemovedPropertyType() + { + await CreateDocumentsAndWaitForIndexing(); + + SearchResult results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(2)); + + _childContentType.RemovePropertyType("title"); + await ContentTypeService.UpdateAsync(_childContentType, Constants.Security.SuperUserKey); + + await WaitForIndexesToRebuild(); + + results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(1)); + } + + [Test] + public async Task CannotSearchForRemovedDocumentType() + { + await CreateDocumentsAndWaitForIndexing(); + + SearchResult results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(2)); + + await ContentTypeService.DeleteAsync(_childContentType.Key, Constants.Security.SuperUserKey); + + await WaitForIndexesToRebuild(); + + results = await Searcher.SearchAsync( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + query: "Home Page"); + + Assert.That(results.Total, Is.EqualTo(1)); + + IContentType? contentType = await ContentTypeService.GetAsync(_childContentType.Key); + Assert.That(contentType, Is.Null); + } + + private async Task CreateDocumentsAndWaitForIndexing() + => await WaitForIndexing( + Cms.Search.Core.Constants.IndexAliases.DraftContent, + async () => await CreateDocuments()); + + private async Task CreateDocuments() + { + ContentTypeCreateModel parentContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType( + "parentType", + "Parent Type"); + Attempt parentContentTypeAttempt = await ContentTypeEditingService.CreateAsync( + parentContentTypeCreateModel, + Constants.Security.SuperUserKey); + Assert.That(parentContentTypeAttempt.Success, Is.True); + _parentContentType = parentContentTypeAttempt.Result!; + + // Create Child ContentType + ContentTypeCreateModel childContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType( + "childType", + "Child Type"); + Attempt childContentTypeAttempt = await ContentTypeEditingService.CreateAsync( + childContentTypeCreateModel, + Constants.Security.SuperUserKey); + Assert.That(childContentTypeAttempt.Success, Is.True); + _childContentType = childContentTypeAttempt.Result!; + + // Update Parent ContentType to allow Child ContentType + ContentTypeUpdateModel parentContentTypeUpdateModel = ContentTypeUpdateHelper.CreateContentTypeUpdateModel(_parentContentType); + parentContentTypeUpdateModel.AllowedContentTypes = + [ + new ContentTypeSort(_childContentType.Key, 0, childContentTypeCreateModel.Alias) + ]; + Attempt updatedParentResult = await ContentTypeEditingService.UpdateAsync( + _parentContentType, + parentContentTypeUpdateModel, + Constants.Security.SuperUserKey); + Assert.That(updatedParentResult.Success, Is.True); + + // Create Root Document (Parent) + ContentCreateModel rootCreateModel = ContentEditingBuilder.CreateSimpleContent(_parentContentType.Key, "Root Document"); + Attempt createRootResult = await ContentEditingService.CreateAsync(rootCreateModel, Constants.Security.SuperUserKey); + Assert.That(createRootResult.Success, Is.True); + IContent? rootDocument = createRootResult.Result.Content; + + // Create Child Document under Root + ContentCreateModel childCreateModel = ContentEditingBuilder.CreateSimpleContent( + _childContentType.Key, + "Child Document", + rootDocument!.Key); + Attempt createChildResult = await ContentEditingService.CreateAsync(childCreateModel, Constants.Security.SuperUserKey); + Assert.That(createChildResult.Success, Is.True); + } + + private async Task WaitForIndexesToRebuild() + => await Task.Delay(3000); +}