Elements: Split content type validation for create and update (#21906)

* Split content type validation for create and update to allow saving elements no longer permitted in library

* Add integration test for element update after AllowedInLibrary toggle

Verify that ElementEditingService.UpdateAsync succeeds when the content
type's AllowedInLibrary flag is set to false after the element was
created, covering the split validation introduced for create vs update.

* Move content type validation into TryGetAndValidateContentType override

Eliminate redundant content type lookups in CreateAsync and UpdateAsync
by moving the IsElement/AllowedInLibrary check into the
TryGetAndValidateContentType override, which distinguishes create from
update by checking if the model is a ContentCreationModelBase.

* Use Assert.Multiple for element property assertions in update test

* Extract IsAllowedLibraryElement static method for readability
This commit is contained in:
Laura Neto
2026-03-06 17:34:00 +00:00
committed by GitHub
parent a17aef5af8
commit 421ad35034
2 changed files with 79 additions and 21 deletions
@@ -86,40 +86,37 @@ internal sealed class ElementEditingService
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateUpdateAsync(Guid key, ValidateElementUpdateModel updateModel, Guid userKey)
{
IElement? content = _elementService.GetById(key);
return content is not null
? await ValidateCulturesAndPropertiesAsync(
updateModel,
content.ContentType.Key,
updateModel.Cultures,
userKey)
: Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult());
if (content is null)
{
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult());
}
return await ValidateCulturesAndPropertiesAsync(
updateModel,
content.ContentType.Key,
updateModel.Cultures,
userKey);
}
/// <inheritdoc />
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateCreateAsync(ElementCreateModel createModel, Guid userKey)
=> await ValidateCulturesAndPropertiesAsync(
createModel,
createModel.ContentTypeKey,
createModel.Variants.Select(variant => variant.Culture),
userKey);
protected override IContentType? TryGetAndValidateContentType(
Guid contentTypeKey, ContentEditingModelBase contentEditingModelBase,
out ContentEditingOperationStatus operationStatus)
{
IContentType? contentType = base.TryGetAndValidateContentType(contentTypeKey, contentEditingModelBase, out operationStatus);
IContentType? contentType = ContentTypeService.Get(createModel.ContentTypeKey);
if (contentType is null)
{
return null;
return Attempt.FailWithStatus(ContentEditingOperationStatus.ContentTypeNotFound, new ContentValidationResult());
}
if (contentType.IsElement is false || contentType.AllowedInLibrary is false)
{
operationStatus = ContentEditingOperationStatus.NotAllowed;
return null;
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotAllowed, new ContentValidationResult());
}
return contentType;
return await ValidateCulturesAndPropertiesAsync(
createModel,
createModel.ContentTypeKey,
createModel.Variants.Select(variant => variant.Culture),
userKey);
}
public async Task<Attempt<ElementCreateResult, ContentEditingOperationStatus>> CreateAsync(ElementCreateModel createModel, Guid userKey)
@@ -192,6 +189,27 @@ internal sealed class ElementEditingService
protected override IElement New(string? name, int parentId, IContentType contentType)
=> new Element(name, parentId, contentType);
protected override IContentType? TryGetAndValidateContentType(
Guid contentTypeKey, ContentEditingModelBase contentEditingModelBase,
out ContentEditingOperationStatus operationStatus)
{
IContentType? contentType = base.TryGetAndValidateContentType(contentTypeKey, contentEditingModelBase, out operationStatus);
if (contentType is null)
{
return null;
}
// Only enforce IsElement + AllowedInLibrary on create; updates only need the content type to exist
if (contentEditingModelBase is ContentCreationModelBase
&& IsAllowedLibraryElement(contentType) is false)
{
operationStatus = ContentEditingOperationStatus.NotAllowed;
return null;
}
return contentType;
}
protected override async Task<(int? ParentId, ContentEditingOperationStatus OperationStatus)> TryGetAndValidateParentIdAsync(Guid? parentKey, IContentType contentType)
{
if (parentKey.HasValue is false)
@@ -497,4 +515,7 @@ internal sealed class ElementEditingService
return ContentEditingOperationStatus.Unknown;
}
}
private static bool IsAllowedLibraryElement(IContentType contentType)
=> contentType.IsElement && contentType.AllowedInLibrary;
}
@@ -126,6 +126,43 @@ public partial class ElementEditingServiceTests
}
}
[Test]
public async Task Can_Update_Element_After_ContentType_Disallowed_From_Library()
{
var elementType = await CreateInvariantElementType();
var element = await CreateInvariantElement(contentTypeKey: elementType.Key);
// Disallow the content type from the library after the element has been created
elementType.AllowedInLibrary = false;
await ContentTypeService.UpdateAsync(elementType, Constants.Security.SuperUserKey);
var updateModel = new ElementUpdateModel
{
Variants =
[
new VariantModel { Name = "Updated Name" }
],
Properties =
[
new PropertyValueModel { Alias = "title", Value = "The updated title" },
new PropertyValueModel { Alias = "text", Value = "The updated text" }
],
};
var result = await ElementEditingService.UpdateAsync(element.Key, updateModel, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status);
var updatedElement = result.Result.Content;
Assert.IsNotNull(updatedElement);
Assert.Multiple(() =>
{
Assert.AreEqual("Updated Name", updatedElement.Name);
Assert.AreEqual("The updated title", updatedElement.GetValue<string>("title"));
Assert.AreEqual("The updated text", updatedElement.GetValue<string>("text"));
});
}
[Test]
public async Task Can_Update_Culture_And_Segment_Variant()
{