Only prevent the unpublish or delete of a related item when configured to do so if it is related as a child, not as a parent (#18886)

* Only prevent the unpubkish or delete of a related item when configured to do so if it is related as a child, not as a parent.

* Fixed incorect parameter names.

* Fixed failing integration tests.

* Use using variable instead to reduce nesting

* Applied suggestions from code review.

* Used simple using statement throughout RelationService for consistency.

* Applied XML header comments consistently.

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Andy Butland
2025-04-01 15:49:49 +02:00
committed by GitHub
parent bf89eae07f
commit 8e0912cbf1
11 changed files with 343 additions and 289 deletions

View File

@@ -1,8 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
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;
@@ -12,29 +11,20 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentEditingServiceTests
{
protected IRelationService RelationService => GetRequiredService<IRelationService>();
public static void ConfigureDisableDeleteWhenReferenced(IUmbracoBuilder builder)
{
builder.Services.Configure<ContentSettings>(config =>
=> builder.Services.Configure<ContentSettings>(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()
public async Task Cannot_Delete_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
Relate(Subpage, Subpage2);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage2, Subpage);
var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.CannotDeleteWhenReferenced, result.Status);
@@ -44,6 +34,24 @@ public partial class ContentEditingServiceTests
Assert.IsNotNull(subpage);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableDeleteWhenReferenced))]
public async Task Can_Delete_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage, Subpage2);
var result = await ContentEditingService.DeleteFromRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
// re-get and verify deleted
var subpage = await ContentEditingService.GetAsync(Subpage.Key);
Assert.IsNull(subpage);
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Delete_FromRecycleBin(bool variant)

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -9,12 +9,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentEditingServiceTests
{
public static void ConfigureDisableDelete(IUmbracoBuilder builder)
{
builder.Services.Configure<ContentSettings>(config =>
public static new void ConfigureDisableUnpublishWhenReferencedTrue(IUmbracoBuilder builder)
=> builder.Services.Configure<ContentSettings>(config =>
config.DisableUnpublishWhenReferenced = true);
}
[TestCase(true)]
[TestCase(false)]
@@ -33,10 +30,11 @@ public partial class ContentEditingServiceTests
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableDelete))]
public async Task Cannot_Move_To_Recycle_Bin_If_Referenced()
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Cannot_Move_To_Recycle_Bin_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
Relate(Subpage, Subpage2);
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage2, Subpage);
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsFalse(moveAttempt.Success);
Assert.AreEqual(ContentEditingOperationStatus.CannotMoveToRecycleBinWhenReferenced, moveAttempt.Status);
@@ -47,6 +45,22 @@ public partial class ContentEditingServiceTests
Assert.IsFalse(content.Trashed);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Can_Move_To_Recycle_Bin_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
// Setup a relation where the page being deleted is related to another page as a child (e.g. the other page has a picker and has selected this page).
Relate(Subpage, Subpage2);
var moveAttempt = await ContentEditingService.MoveToRecycleBinAsync(Subpage.Key, Constants.Security.SuperUserKey);
Assert.IsTrue(moveAttempt.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, moveAttempt.Status);
// re-get and verify moved
var content = await ContentEditingService.GetAsync(Subpage.Key);
Assert.IsNotNull(content);
Assert.IsTrue(content.Trashed);
}
[Test]
public async Task Cannot_Move_Non_Existing_To_Recycle_Bin()
{

View File

@@ -16,6 +16,14 @@ public partial class ContentEditingServiceTests : ContentEditingServiceTestsBase
[SetUp]
public void Setup() => ContentRepositoryBase.ThrowOnWarning = true;
public void Relate(IContent parent, IContent child)
{
var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var relation = RelationService.Relate(parent.Id, child.Id, relatedContentRelType);
RelationService.Save(relation);
}
protected override void CustomTestSetup(IUmbracoBuilder builder)
=> builder.AddNotificationHandler<ContentCopiedNotification, RelateOnCopyNotificationHandler>();

View File

@@ -1,14 +1,22 @@
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.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Integration.Attributes;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
public partial class ContentPublishingServiceTests
{
public static new void ConfigureDisableUnpublishWhenReferencedTrue(IUmbracoBuilder builder)
=> builder.Services.Configure<ContentSettings>(config =>
config.DisableUnpublishWhenReferenced = true);
[Test]
public async Task Can_Unpublish_Root()
{
@@ -92,6 +100,40 @@ public partial class ContentPublishingServiceTests
VerifyIsPublished(Textpage.Key);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Cannot_Unpublish_When_Content_Is_Related_As_A_Child_And_Configured_To_Disable_When_Related()
{
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey);
VerifyIsPublished(Textpage.Key);
// Setup a relation where the page being unpublished is related to another page as a child (e.g. the other page has a picker and has selected this page).
RelationService.Relate(Subpage, Textpage, Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentPublishingOperationStatus.CannotUnpublishWhenReferenced, result.Result);
VerifyIsPublished(Textpage.Key);
}
[Test]
[ConfigureBuilder(ActionName = nameof(ConfigureDisableUnpublishWhenReferencedTrue))]
public async Task Can_Unpublish_When_Content_Is_Related_As_A_Parent_And_Configured_To_Disable_When_Related()
{
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey);
VerifyIsPublished(Textpage.Key);
// Setup a relation where the page being unpublished is related to another page as a parent (e.g. this page has a picker and has selected the other page).
RelationService.Relate(Textpage, Subpage, Constants.Conventions.RelationTypes.RelatedDocumentAlias);
var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result);
VerifyIsNotPublished(Textpage.Key);
}
[Test]
public async Task Can_Unpublish_Single_Culture()
{
@@ -229,8 +271,6 @@ public partial class ContentPublishingServiceTests
Assert.AreEqual(0, content.PublishedCultures.Count());
}
[Test]
public async Task Can_Unpublish_Non_Mandatory_Cultures()
{

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
@@ -20,6 +20,8 @@ public partial class ContentPublishingServiceTests : UmbracoIntegrationTestWithC
{
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
private IRelationService RelationService => GetRequiredService<IRelationService>();
private static readonly ISet<string> _allCultures = new HashSet<string>(){ "*" };
private static CultureAndScheduleModel MakeModel(ISet<string> cultures) => new CultureAndScheduleModel()