diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index 0b527a9b39..e3bd948632 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -76,6 +76,14 @@ public abstract class ContentControllerBase : ManagementApiControllerBase .WithTitle("Duplicate name") .WithDetail("The supplied name is already in use for the same content type.") .Build()), + ContentEditingOperationStatus.CannotDeleteWhenReferenced => BadRequest(problemDetailsBuilder + .WithTitle("Cannot delete a referenced content item") + .WithDetail("Cannot delete a referenced document, while the setting ContentSettings.DisableDeleteWhenReferenced is enabled.") + .Build()), + ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced => BadRequest(problemDetailsBuilder + .WithTitle("Cannot move a referenced document to the recycle bin") + .WithDetail("Cannot move a referenced document to the recycle bin, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.") + .Build()), ContentEditingOperationStatus.Unknown => StatusCode( StatusCodes.Status500InternalServerError, problemDetailsBuilder diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs index 8789eab687..52b2f34d7f 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs @@ -121,6 +121,11 @@ public abstract class DocumentControllerBase : ContentControllerBase .WithDetail( "Cannot handle an unpublish time that is not after the current server time.") .Build()), + ContentPublishingOperationStatus.CannotUnpublishWhenReferenced => BadRequest(problemDetailsBuilder + .WithTitle("Cannot unpublish document when it's referenced somewhere else.") + .WithDetail( + "Cannot unpublish a referenced document, while the setting ContentSettings.DisableUnpublishWhenReferenced is enabled.") + .Build()), ContentPublishingOperationStatus.FailedBranch => BadRequest(problemDetailsBuilder .WithTitle("Failed branch operation") .WithDetail("One or more items in the branch could not complete the operation.") diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index 7848c34ecb..44fdc4baca 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -21,8 +23,10 @@ internal sealed class ContentBlueprintEditingService ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, IContentValidationService validationService, - IContentBlueprintContainerService containerService) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, validationService) + IContentBlueprintContainerService containerService, + IOptionsMonitor optionsMonitor, + IRelationService relationService) + : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, validationService, optionsMonitor, relationService) => _containerService = containerService; public async Task GetAsync(Guid key) diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 73280efaf1..1dc54426d0 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -20,7 +20,6 @@ internal sealed class ContentEditingService private readonly IUserService _userService; private readonly ILocalizationService _localizationService; private readonly ILanguageService _languageService; - private readonly ContentSettings _contentSettings; public ContentEditingService( IContentService contentService, @@ -36,8 +35,20 @@ internal sealed class ContentEditingService IUserService userService, ILocalizationService localizationService, ILanguageService languageService, - IOptions contentSettings) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, contentValidationService, treeEntitySortingService) + IOptionsMonitor optionsMonitor, + IRelationService relationService) + : base( + contentService, + contentTypeService, + propertyEditorCollection, + dataTypeService, + logger, + scopeProvider, + userIdKeyResolver, + contentValidationService, + treeEntitySortingService, + optionsMonitor, + relationService) { _propertyEditorCollection = propertyEditorCollection; _templateService = templateService; @@ -45,7 +56,6 @@ internal sealed class ContentEditingService _userService = userService; _localizationService = localizationService; _languageService = languageService; - _contentSettings = contentSettings.Value; } public async Task GetAsync(Guid key) @@ -169,7 +179,7 @@ internal sealed class ContentEditingService } // If property does not support merging, we still need to overwrite if we are not allowed to edit invariant properties. - if (_contentSettings.AllowEditInvariantFromNonDefault is false && allowedToEditDefaultLanguage is false) + if (ContentSettings.AllowEditInvariantFromNonDefault is false && allowedToEditDefaultLanguage is false) { foreach (IProperty property in invariantProperties) { @@ -192,7 +202,7 @@ internal sealed class ContentEditingService var mergedValue = propertyWithEditor.DataEditor.MergeVariantInvariantPropertyValue( currentValue, editedValue, - _contentSettings.AllowEditInvariantFromNonDefault || (defaultLanguage is not null && allowedCultures.Contains(defaultLanguage.IsoCode)), + ContentSettings.AllowEditInvariantFromNonDefault || (defaultLanguage is not null && allowedCultures.Contains(defaultLanguage.IsoCode)), allowedCultures); propertyWithEditor.Property.SetValue(mergedValue, null, null); @@ -243,10 +253,10 @@ internal sealed class ContentEditingService => await HandleMoveToRecycleBinAsync(key, userKey); public async Task> DeleteFromRecycleBinAsync(Guid key, Guid userKey) - => await HandleDeleteAsync(key, userKey, true); + => await HandleDeleteAsync(key, userKey,true); public async Task> DeleteAsync(Guid key, Guid userKey) - => await HandleDeleteAsync(key, userKey, false); + => await HandleDeleteAsync(key, userKey,false); public async Task> MoveAsync(Guid key, Guid? parentKey, Guid userKey) => await HandleMoveAsync(key, parentKey, userKey); @@ -303,11 +313,9 @@ internal sealed class ContentEditingService protected override IContent? Copy(IContent content, int newParentId, bool relateToOriginal, bool includeDescendants, int userId) => ContentService.Copy(content, newParentId, relateToOriginal, includeDescendants, userId); - protected override OperationResult? MoveToRecycleBin(IContent content, int userId) - => ContentService.MoveToRecycleBin(content, userId); + protected override OperationResult? MoveToRecycleBin(IContent content, int userId) => ContentService.MoveToRecycleBin(content, userId); - protected override OperationResult? Delete(IContent content, int userId) - => ContentService.Delete(content, userId); + protected override OperationResult? Delete(IContent content, int userId) => ContentService.Delete(content, userId); protected override IEnumerable GetPagedChildren(int parentId, int pageIndex, int pageSize, out long total) => ContentService.GetPagedChildren(parentId, pageIndex, pageSize, out total); diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 1c0c5f7ff6..63232f7262 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Editors; @@ -20,6 +22,7 @@ internal abstract class ContentEditingServiceBase> _logger; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IContentValidationServiceBase _validationService; + private readonly IRelationService _relationService; protected ContentEditingServiceBase( TContentService contentService, @@ -29,13 +32,22 @@ internal abstract class ContentEditingServiceBase> logger, ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, - IContentValidationServiceBase validationService) + IContentValidationServiceBase validationService, + IOptionsMonitor optionsMonitor, + IRelationService relationService) { _propertyEditorCollection = propertyEditorCollection; _dataTypeService = dataTypeService; _logger = logger; _userIdKeyResolver = userIdKeyResolver; _validationService = validationService; + ContentSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange((contentSettings) => + { + ContentSettings = contentSettings; + }); + + _relationService = relationService; CoreScopeProvider = scopeProvider; ContentService = contentService; ContentTypeService = contentTypeService; @@ -51,6 +63,8 @@ internal abstract class ContentEditingServiceBase> HandleMoveToRecycleBinAsync(Guid key, Guid userKey) - => await HandleDeletionAsync(key, userKey, ContentTrashStatusRequirement.MustNotBeTrashed, MoveToRecycleBin); + => await HandleDeletionAsync(key, + userKey, + ContentTrashStatusRequirement.MustNotBeTrashed, + MoveToRecycleBin, + ContentSettings.DisableUnpublishWhenReferenced, + ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced); protected async Task> HandleDeleteAsync(Guid key, Guid userKey, bool mustBeTrashed = true) - => await HandleDeletionAsync(key, userKey, mustBeTrashed ? ContentTrashStatusRequirement.MustBeTrashed : ContentTrashStatusRequirement.Irrelevant, Delete); + => await HandleDeletionAsync(key, + userKey, + mustBeTrashed + ? ContentTrashStatusRequirement.MustBeTrashed + : ContentTrashStatusRequirement.Irrelevant, + Delete, + ContentSettings.DisableDeleteWhenReferenced, + ContentEditingOperationStatus.CannotDeleteWhenReferenced); // helper method to perform move-to-recycle-bin, delete-from-recycle-bin and delete for content as they are very much handled in the same way // IContentEditingService methods hitting this (ContentTrashStatusRequirement, calledFunction): // DeleteAsync (irrelevant, Delete) // MoveToRecycleBinAsync (MustNotBeTrashed, MoveToRecycleBin) // DeleteFromRecycleBinAsync (MustBeTrashed, Delete) - private async Task> HandleDeletionAsync(Guid key, Guid userKey, ContentTrashStatusRequirement trashStatusRequirement, Func performDelete) + private async Task> HandleDeletionAsync( + Guid key, + Guid userKey, + ContentTrashStatusRequirement trashStatusRequirement, + Func performDelete, + bool disabledWhenReferenced, + ContentEditingOperationStatus referenceFailStatus) { using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); TContent? content = ContentService.GetById(key); @@ -166,6 +198,11 @@ internal abstract class ContentEditingServiceBase(status, content)); } + if (disabledWhenReferenced && _relationService.IsRelated(content.Id)) + { + return Attempt.FailWithStatus(referenceFailStatus, content); + } + var userId = await GetUserIdAsync(userKey); OperationResult? deleteResult = performDelete(content, userId); @@ -264,6 +301,7 @@ internal abstract class ContentEditingServiceBase ContentEditingOperationStatus.Success, OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification, + OperationResultType.FailedCannot => ContentEditingOperationStatus.CannotDeleteWhenReferenced, // for any other state we'll return "unknown" so we know that we need to amend this switch statement _ => ContentEditingOperationStatus.Unknown @@ -479,7 +517,7 @@ internal abstract class ContentEditingServiceBase /// Should never be made public, serves the purpose of a nullable bool but more readable. /// - private enum ContentTrashStatusRequirement + protected internal enum ContentTrashStatusRequirement { Irrelevant, MustBeTrashed, diff --git a/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs index 523b38f154..47335566bd 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceWithSortingBase.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -26,7 +28,9 @@ internal abstract class ContentEditingServiceWithSortingBase validationService, - ITreeEntitySortingService treeEntitySortingService) + ITreeEntitySortingService treeEntitySortingService, + IOptionsMonitor optionsMonitor, + IRelationService relationService) : base( contentService, contentTypeService, @@ -35,7 +39,9 @@ internal abstract class ContentEditingServiceWithSortingBase optionsMonitor, + IRelationService relationService) { _coreScopeProvider = coreScopeProvider; _contentService = contentService; @@ -31,6 +37,12 @@ internal sealed class ContentPublishingService : IContentPublishingService _contentValidationService = contentValidationService; _contentTypeService = contentTypeService; _languageService = languageService; + _relationService = relationService; + _contentSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange((contentSettings) => + { + _contentSettings = contentSettings; + }); } /// @@ -299,6 +311,12 @@ internal sealed class ContentPublishingService : IContentPublishingService return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound); } + if (_contentSettings.DisableUnpublishWhenReferenced && _relationService.IsRelated(content.Id)) + { + scope.Complete(); + return Attempt.Fail(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced); + } + var userId = await _userIdKeyResolver.GetAsync(userKey); // If cultures are provided for non variant content, and they include the default culture, consider diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 0ee1084252..581159ab64 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -37,6 +39,8 @@ public class ContentService : RepositoryService, IContentService private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly PropertyEditorCollection _propertyEditorCollection; private readonly IIdKeyMap _idKeyMap; + private ContentSettings _contentSettings; + private readonly IRelationService _relationService; private IQuery? _queryNotTrashed; #region Constructors @@ -56,7 +60,9 @@ public class ContentService : RepositoryService, IContentService ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, PropertyEditorCollection propertyEditorCollection, - IIdKeyMap idKeyMap) + IIdKeyMap idKeyMap, + IOptionsMonitor optionsMonitor, + IRelationService relationService) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -71,9 +77,53 @@ public class ContentService : RepositoryService, IContentService _userIdKeyResolver = userIdKeyResolver; _propertyEditorCollection = propertyEditorCollection; _idKeyMap = idKeyMap; + _contentSettings = optionsMonitor.CurrentValue; + optionsMonitor.OnChange((contentSettings) => + { + _contentSettings = contentSettings; + }); + _relationService = relationService; _logger = loggerFactory.CreateLogger(); } + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V17.")] + + public ContentService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver, + PropertyEditorCollection propertyEditorCollection, + IIdKeyMap idKeyMap) + : this( + provider, + loggerFactory, + eventMessagesFactory, + documentRepository, + entityRepository, + auditRepository, + contentTypeRepository, + documentBlueprintRepository, + languageRepository, + propertyValidationService, + shortStringHelper, + cultureImpactFactory, + userIdKeyResolver, + propertyEditorCollection, + idKeyMap, + StaticServiceProvider.Instance.GetRequiredService>(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } [Obsolete("Use non-obsolete constructor. Scheduled for removal in V17.")] public ContentService( ICoreScopeProvider provider, @@ -2718,6 +2768,11 @@ public class ContentService : RepositoryService, IContentService { foreach (IContent content in contents) { + if (_contentSettings.DisableDeleteWhenReferenced && _relationService.IsRelated(content.Id)) + { + continue; + } + DeleteLocked(scope, content, eventMessages); deleted.Add(content); } diff --git a/src/Umbraco.Core/Services/MediaEditingService.cs b/src/Umbraco.Core/Services/MediaEditingService.cs index 6484d1112b..2a27c048a6 100644 --- a/src/Umbraco.Core/Services/MediaEditingService.cs +++ b/src/Umbraco.Core/Services/MediaEditingService.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; @@ -21,8 +23,21 @@ internal sealed class MediaEditingService ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, ITreeEntitySortingService treeEntitySortingService, - IMediaValidationService mediaValidationService) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, mediaValidationService, treeEntitySortingService) + IMediaValidationService mediaValidationService, + IOptionsMonitor optionsMonitor, + IRelationService relationService) + : base( + contentService, + contentTypeService, + propertyEditorCollection, + dataTypeService, + logger, + scopeProvider, + userIdKeyResolver, + mediaValidationService, + treeEntitySortingService, + optionsMonitor, + relationService) => _logger = logger; public async Task GetAsync(Guid key) diff --git a/src/Umbraco.Core/Services/MemberContentEditingService.cs b/src/Umbraco.Core/Services/MemberContentEditingService.cs index d53121fe6a..9e44c9bb16 100644 --- a/src/Umbraco.Core/Services/MemberContentEditingService.cs +++ b/src/Umbraco.Core/Services/MemberContentEditingService.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -24,8 +26,10 @@ internal sealed class MemberContentEditingService ICoreScopeProvider scopeProvider, IUserIdKeyResolver userIdKeyResolver, IMemberValidationService memberValidationService, - IUserService userService) - : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, memberValidationService) + IUserService userService, + IOptionsMonitor optionsMonitor, + IRelationService relationService) + : base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, memberValidationService, optionsMonitor, relationService) { _logger = logger; _userService = userService; diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentEditingOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentEditingOperationStatus.cs index d7f02f48fb..81e9547a84 100644 --- a/src/Umbraco.Core/Services/OperationStatus/ContentEditingOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/ContentEditingOperationStatus.cs @@ -21,4 +21,6 @@ public enum ContentEditingOperationStatus DuplicateKey, DuplicateName, Unknown, + CannotDeleteWhenReferenced, + CannotMoveToRecycleBinWhenReferenced, } diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs index 1c61767ffa..25c9a26389 100644 --- a/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs @@ -26,4 +26,6 @@ public enum ContentPublishingOperationStatus FailedBranch, Failed, // unspecified failure (can happen on unpublish at the time of writing) Unknown, + CannotUnpublishWhenReferenced, + } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs index 1639fbf726..93395a768e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Delete.cs @@ -1,11 +1,49 @@ -using NUnit.Framework; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Integration.Attributes; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; public partial class ContentEditingServiceTests { + protected IRelationService RelationService => GetRequiredService(); + public static void ConfigureDisableDeleteWhenReferenced(IUmbracoBuilder builder) + { + builder.Services.Configure(config => + config.DisableDeleteWhenReferenced = true); + } + + public void Relate(IContent child, IContent parent) + { + var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + + var relation = RelationService.Relate(child.Id, parent.Id, relatedContentRelType); + RelationService.Save(relation); + } + + + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))] + public async Task Cannot_Delete_Referenced_Content() + { + var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey); + Assert.IsTrue(moveAttempt.Success); + + Relate(Subpage, Subpage2); + var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.CannotDeleteWhenReferenced, result.Status); + + // re-get and verify not deleted + var subpage = await ContentEditingService.GetAsync(Subpage.Key); + Assert.IsNotNull(subpage); + } + [TestCase(true)] [TestCase(false)] public async Task Can_Delete_FromRecycleBin(bool variant) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecycleBin.cs index 3dd806abfe..ac7700131f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.MoveToRecycleBin.cs @@ -1,11 +1,21 @@ -using NUnit.Framework; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Integration.Attributes; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; public partial class ContentEditingServiceTests { + + public static void ConfigureDisableDelete(IUmbracoBuilder builder) + { + builder.Services.Configure(config => + config.DisableUnpublishWhenReferenced = true); + } + [TestCase(true)] [TestCase(false)] public async Task Can_Move_To_Recycle_Bin(bool variant) @@ -22,6 +32,21 @@ public partial class ContentEditingServiceTests Assert.IsTrue(content.Trashed); } + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureDisableDelete))] + public async Task Cannot_Move_To_Recycle_Bin_If_Referenced() + { + Relate(Subpage, Subpage2); + var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey); + Assert.IsFalse(moveAttempt.Success); + Assert.AreEqual(ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced, moveAttempt.Status); + + // re-get and verify not moved + var content = await ContentEditingService.GetAsync(Subpage.Key); + Assert.IsNotNull(content); + Assert.IsFalse(content.Trashed); + } + [Test] public async Task Cannot_Move_Non_Existing_To_Recycle_Bin() {