diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateCreateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateCreateDocumentController.cs index ecca82286c..7087e119f7 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateCreateDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateCreateDocumentController.cs @@ -1,11 +1,14 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; @@ -16,15 +19,32 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase { private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory; private readonly IContentEditingService _contentEditingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public ValidateCreateDocumentController( IAuthorizationService authorizationService, IDocumentEditingPresentationFactory documentEditingPresentationFactory, IContentEditingService contentEditingService) + : this( + authorizationService, + documentEditingPresentationFactory, + contentEditingService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [ActivatorUtilitiesConstructor] + public ValidateCreateDocumentController( + IAuthorizationService authorizationService, + IDocumentEditingPresentationFactory documentEditingPresentationFactory, + IContentEditingService contentEditingService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) : base(authorizationService) { _documentEditingPresentationFactory = documentEditingPresentationFactory; _contentEditingService = contentEditingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } [HttpPost("validate")] @@ -36,7 +56,10 @@ public class ValidateCreateDocumentController : CreateDocumentControllerBase => await HandleRequest(requestModel, async () => { ContentCreateModel model = _documentEditingPresentationFactory.MapCreateModel(requestModel); - Attempt result = await _contentEditingService.ValidateCreateAsync(model); + Attempt result = + await _contentEditingService.ValidateCreateAsync( + model, + CurrentUserKey(_backOfficeSecurityAccessor)); return result.Success ? Ok() diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs index 05f029f582..bbc54805d8 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs @@ -1,11 +1,14 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; @@ -17,15 +20,32 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase { private readonly IContentEditingService _contentEditingService; private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public ValidateUpdateDocumentController( IAuthorizationService authorizationService, IContentEditingService contentEditingService, IDocumentEditingPresentationFactory documentEditingPresentationFactory) + : this( + authorizationService, + contentEditingService, + documentEditingPresentationFactory, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [ActivatorUtilitiesConstructor] + public ValidateUpdateDocumentController( + IAuthorizationService authorizationService, + IContentEditingService contentEditingService, + IDocumentEditingPresentationFactory documentEditingPresentationFactory, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor) : base(authorizationService) { _contentEditingService = contentEditingService; _documentEditingPresentationFactory = documentEditingPresentationFactory; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; } [HttpPut("{id:guid}/validate")] @@ -62,7 +82,11 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase => await HandleRequest(id, requestModel, async () => { ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel); - Attempt result = await _contentEditingService.ValidateUpdateAsync(id, model); + Attempt result = + await _contentEditingService.ValidateUpdateAsync( + id, + model, + CurrentUserKey(_backOfficeSecurityAccessor)); return result.Success ? Ok() diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 1dc54426d0..49ec63a7d7 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -64,7 +64,7 @@ internal sealed class ContentEditingService return await Task.FromResult(content); } - [Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")] + [Obsolete("Please use the validate update method that is not obsoleted. Scheduled for removal in V16.")] public async Task> ValidateUpdateAsync(Guid key, ContentUpdateModel updateModel) { IContent? content = ContentService.GetById(key); @@ -73,16 +73,50 @@ internal sealed class ContentEditingService : Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); } + [Obsolete("Please use the validate update method that is not obsoleted. Scheduled for removal in V17.")] public async Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel) + => await ValidateUpdateAsync(key, updateModel, Guid.Empty); + + public async Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel, Guid userKey) { IContent? content = ContentService.GetById(key); return content is not null - ? await ValidateCulturesAndPropertiesAsync(updateModel, content.ContentType.Key, updateModel.Cultures) + ? await ValidateCulturesAndPropertiesAsync(updateModel, content.ContentType.Key, await GetCulturesToValidate(updateModel.Cultures, userKey)) : Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); } + [Obsolete("Please use the validate create method that is not obsoleted. Scheduled for removal in V17.")] public async Task> ValidateCreateAsync(ContentCreateModel createModel) - => await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey, createModel.Variants.Select(variant => variant.Culture)); + => await ValidateCreateAsync(createModel, Guid.Empty); + + public async Task> ValidateCreateAsync(ContentCreateModel createModel, Guid userKey) + => await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey, await GetCulturesToValidate(createModel.Variants.Select(variant => variant.Culture), userKey)); + + private async Task?> GetCulturesToValidate(IEnumerable? cultures, Guid userKey) + { + // Cultures to validate can be provided by the calling code, but if the editor is restricted to only have + // access to certain languages, we don't want to validate by any they aren't allowed to edit. + + // TODO: Remove this check once the obsolete overloads to ValidateCreateAsync and ValidateUpdateAsync that don't provide a user key are removed. + // We only have this to ensure backwards compatibility with the obsolete overloads. + if (userKey == Guid.Empty) + { + return cultures; + } + + HashSet? allowedCultures = await GetAllowedCulturesForEditingUser(userKey); + + if (cultures == null) + { + // If no cultures are provided, we are asking to validate all cultures. But if the user doesn't have access to all, we + // should only validate the ones they do. + var allCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToList(); + return allowedCultures.Count == allCultures.Count ? null : allowedCultures; + } + + // If explicit cultures are provided, we should only validate the ones the user has access to. + return cultures.Where(x => !string.IsNullOrEmpty(x) && allowedCultures.Contains(x)).ToList(); + } public async Task> CreateAsync(ContentCreateModel createModel, Guid userKey) { @@ -127,16 +161,7 @@ internal sealed class ContentEditingService IContent? existingContent = await GetAsync(contentWithPotentialUnallowedChanges.Key); - IUser? user = await _userService.GetAsync(userKey); - - if (user is null) - { - return contentWithPotentialUnallowedChanges; - } - - var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!; - - var allowedCultures = (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet(); + HashSet? allowedCultures = await GetAllowedCulturesForEditingUser(userKey); ILanguage? defaultLanguage = await _languageService.GetDefaultLanguageAsync(); @@ -211,6 +236,16 @@ internal sealed class ContentEditingService return contentWithPotentialUnallowedChanges; } + private async Task> GetAllowedCulturesForEditingUser(Guid userKey) + { + IUser? user = await _userService.GetAsync(userKey) + ?? throw new InvalidOperationException($"Could not find user by key {userKey} when editing or validating content."); + + var allowedLanguageIds = user.CalculateAllowedLanguageIds(_localizationService)!; + + return (await _languageService.GetIsoCodesByIdsAsync(allowedLanguageIds)).ToHashSet(); + } + public async Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey) { IContent? content = ContentService.GetById(key); diff --git a/src/Umbraco.Core/Services/IContentEditingService.cs b/src/Umbraco.Core/Services/IContentEditingService.cs index 260e5ae934..3dab432393 100644 --- a/src/Umbraco.Core/Services/IContentEditingService.cs +++ b/src/Umbraco.Core/Services/IContentEditingService.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services.OperationStatus; @@ -8,13 +8,25 @@ public interface IContentEditingService { Task GetAsync(Guid key); + [Obsolete("Please use the validate create method that is not obsoleted. Scheduled for removal in Umbraco 17.")] Task> ValidateCreateAsync(ContentCreateModel createModel); - [Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")] + Task> ValidateCreateAsync(ContentCreateModel createModel, Guid userKey) +#pragma warning disable CS0618 // Type or member is obsolete + => ValidateCreateAsync(createModel); +#pragma warning restore CS0618 // Type or member is obsolete + + [Obsolete("Please use the validate update method that is not obsoleted. Scheduled for removal in Umbraco 16.")] Task> ValidateUpdateAsync(Guid key, ContentUpdateModel updateModel); + [Obsolete("Please use the validate update method that is not obsoleted. Scheduled for removal in Umbraco 17.")] Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel); + Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel, Guid userKey) +#pragma warning disable CS0618 // Type or member is obsolete + => ValidateUpdateAsync(key, updateModel); +#pragma warning restore CS0618 // Type or member is obsolete + Task> CreateAsync(ContentCreateModel createModel, Guid userKey); Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey); diff --git a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs index c84b118abe..2d78198649 100644 --- a/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -3,6 +3,7 @@ using Moq; using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; @@ -27,6 +28,8 @@ public class UserGroupBuilder { private string _alias; private IEnumerable _allowedSections = Enumerable.Empty(); + private IEnumerable _allowedLanguages = Enumerable.Empty(); + private IEnumerable _granularPermissions = Enumerable.Empty(); private string _icon; private int? _id; private Guid? _key; @@ -95,13 +98,24 @@ public class UserGroupBuilder return this; } - public UserGroupBuilder WithAllowedSections(IList allowedSections) { _allowedSections = allowedSections; return this; } + public UserGroupBuilder WithAllowedLanguages(IList allowedLanguages) + { + _allowedLanguages = allowedLanguages; + return this; + } + + public UserGroupBuilder WithGranularPermissions(IList granularPermissions) + { + _granularPermissions = granularPermissions; + return this; + } + public UserGroupBuilder WithStartContentId(int startContentId) { _startContentId = startContentId; @@ -144,17 +158,40 @@ public class UserGroupBuilder Id = id, Key = key, StartContentId = startContentId, - StartMediaId = startMediaId + StartMediaId = startMediaId, + Permissions = _permissions }; - userGroup.Permissions = _permissions; + BuildAllowedSections(userGroup); + BuildAllowedLanguages(userGroup); + BuildGranularPermissions(userGroup); + return userGroup; + } + + + private void BuildAllowedSections(UserGroup userGroup) + { foreach (var section in _allowedSections) { userGroup.AddAllowedSection(section); } + } - return userGroup; + private void BuildAllowedLanguages(UserGroup userGroup) + { + foreach (var language in _allowedLanguages) + { + userGroup.AddAllowedLanguage(language); + } + } + + private void BuildGranularPermissions(UserGroup userGroup) + { + foreach (var permission in _granularPermissions) + { + userGroup.GranularPermissions.Add(permission); + } } public static UserGroup CreateUserGroup( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs index 5e4e74b793..954012c74a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Update.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; @@ -346,6 +346,7 @@ public partial class ContentEditingServiceTests InvariantName = "Updated Name", InvariantProperties = new[] { + new PropertyValueModel { Alias = "title", Value = "The initial title" }, new PropertyValueModel { Alias = "label", Value = "The updated label value" } } }; @@ -390,6 +391,7 @@ public partial class ContentEditingServiceTests Name = "Updated English Name", Properties = new [] { + new PropertyValueModel { Alias = "variantTitle", Value = "The initial English title" }, new PropertyValueModel { Alias = "variantLabel", Value = "The updated English label value" } } }, @@ -399,6 +401,7 @@ public partial class ContentEditingServiceTests Name = "Updated Danish Name", Properties = new [] { + new PropertyValueModel { Alias = "variantTitle", Value = "The initial Danish title" }, new PropertyValueModel { Alias = "variantLabel", Value = "The updated Danish label value" } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Validate.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Validate.cs new file mode 100644 index 0000000000..1589668a78 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTests.Validate.cs @@ -0,0 +1,191 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public partial class ContentEditingServiceTests +{ + [Test] + public async Task Can_Validate_Valid_Invariant_Content() + { + var content = await CreateInvariantContent(); + + var validateContentUpdateModel = new ValidateContentUpdateModel + { + InvariantName = "Updated Name", + InvariantProperties = + [ + new PropertyValueModel { Alias = "title", Value = "The updated title" }, + new PropertyValueModel { Alias = "text", Value = "The updated text" } + ] + }; + + Attempt result = await ContentEditingService.ValidateUpdateAsync(content.Key, validateContentUpdateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + } + + [Test] + public async Task Will_Fail_Invalid_Invariant_Content() + { + var content = await CreateInvariantContent(); + + var validateContentUpdateModel = new ValidateContentUpdateModel + { + InvariantName = "Updated Name", + InvariantProperties = + [ + new PropertyValueModel { Alias = "title", Value = null }, + new PropertyValueModel { Alias = "text", Value = "The updated text" } + ] + }; + + Attempt result = await ContentEditingService.ValidateUpdateAsync(content.Key, validateContentUpdateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.PropertyValidationError, result.Status); + Assert.AreEqual(1, result.Result.ValidationErrors.Count()); + Assert.AreEqual("#validation_invalidNull", result.Result.ValidationErrors.Single(x => x.Alias == "title").ErrorMessages[0]); + } + + [Test] + public async Task Can_Validate_Valid_Variant_Content() + { + var content = await CreateVariantContent(); + + var validateContentUpdateModel = new ValidateContentUpdateModel + { + InvariantProperties = + [ + new PropertyValueModel { Alias = "invariantTitle", Value = "The updated invariant title" } + ], + Variants = + [ + new VariantModel + { + Culture = "en-US", + Name = "Updated English Name", + Properties = + [ + new PropertyValueModel { Alias = "variantTitle", Value = "The updated English title" } + ] + }, + new VariantModel + { + Culture = "da-DK", + Name = "Updated Danish Name", + Properties = + [ + new PropertyValueModel { Alias = "variantTitle", Value = "The updated Danish title" } + ] + } + ], + }; + + Attempt result = await ContentEditingService.ValidateUpdateAsync(content.Key, validateContentUpdateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + } + + [Test] + public async Task Will_Fail_Invalid_Variant_Content() + { + var content = await CreateVariantContent(); + + var validateContentUpdateModel = new ValidateContentUpdateModel + { + InvariantProperties = + [ + new PropertyValueModel { Alias = "invariantTitle", Value = "The updated invariant title" } + ], + Variants = + [ + new VariantModel + { + Culture = "en-US", + Name = "Updated English Name", + Properties = + [ + new PropertyValueModel { Alias = "variantTitle", Value = "The updated English title" } + ] + }, + new VariantModel + { + Culture = "da-DK", + Name = "Updated Danish Name", + Properties = + [ + new PropertyValueModel { Alias = "variantTitle", Value = null } + ] + } + ], + }; + + Attempt result = await ContentEditingService.ValidateUpdateAsync(content.Key, validateContentUpdateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.PropertyValidationError, result.Status); + Assert.AreEqual(1, result.Result.ValidationErrors.Count()); + Assert.AreEqual("#validation_invalidNull", result.Result.ValidationErrors.Single(x => x.Alias == "variantTitle" && x.Culture == "da-DK").ErrorMessages[0]); + } + + [Test] + public async Task Will_Succeed_For_Invalid_Variant_Content_Without_Access_To_Edited_Culture() + { + var content = await CreateVariantContent(); + + IUser englishEditor = await CreateEnglishLanguageOnlyEditor(); + + var validateContentUpdateModel = new ValidateContentUpdateModel + { + InvariantProperties = + [ + new PropertyValueModel { Alias = "invariantTitle", Value = "The updated invariant title" } + ], + Variants = + [ + new VariantModel + { + Culture = "en-US", + Name = "Updated English Name", + Properties = + [ + new PropertyValueModel { Alias = "variantTitle", Value = "The updated English title" } + ] + } + ], + }; + + Attempt result = await ContentEditingService.ValidateUpdateAsync(content.Key, validateContentUpdateModel, englishEditor.Key); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + } + + private async Task CreateEnglishLanguageOnlyEditor() + { + var enUSLanguage = await LanguageService.GetAsync("en-US"); + var userGroup = new UserGroupBuilder() + .WithName("English Editors") + .WithAlias("englishEditors") + .WithAllowedLanguages([enUSLanguage.Id]) + .Build(); + + var createUserGroupResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(createUserGroupResult.Success); + + var createUserAttempt = await UserService.CreateAsync(Constants.Security.SuperUserKey, new UserCreateModel + { + Email = "english-editor@test.com", + Name = "Test English Editor", + UserName = "english-editor@test.com", + UserGroupKeys = new[] { userGroup.Key }.ToHashSet(), + }); + Assert.IsTrue(createUserAttempt.Success); + + return await UserService.GetAsync(createUserAttempt.Result.CreatedUser.Key); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs index f9dd811f0f..ebad7f9545 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEditingServiceTestsBase.cs @@ -21,7 +21,11 @@ public abstract class ContentEditingServiceTestsBase : UmbracoIntegrationTestWit protected IContentBlueprintEditingService ContentBlueprintEditingService => GetRequiredService(); - private ILanguageService LanguageService => GetRequiredService(); + protected ILanguageService LanguageService => GetRequiredService(); + + protected IUserService UserService => GetRequiredService(); + + protected IUserGroupService UserGroupService => GetRequiredService(); protected IContentType CreateInvariantContentType(params ITemplate[] templates) { @@ -30,22 +34,23 @@ public abstract class ContentEditingServiceTestsBase : UmbracoIntegrationTestWit .WithName("Invariant Test") .WithContentVariation(ContentVariation.Nothing) .AddPropertyType() - .WithAlias("title") - .WithName("Title") - .WithVariations(ContentVariation.Nothing) - .Done() + .WithAlias("title") + .WithName("Title") + .WithMandatory(true) + .WithVariations(ContentVariation.Nothing) + .Done() .AddPropertyType() - .WithAlias("text") - .WithName("Text") - .WithVariations(ContentVariation.Nothing) - .Done() + .WithAlias("text") + .WithName("Text") + .WithVariations(ContentVariation.Nothing) + .Done() .AddPropertyType() - .WithAlias("label") - .WithName("Label") - .WithDataTypeId(Constants.DataTypes.LabelString) - .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label) - .WithVariations(ContentVariation.Nothing) - .Done(); + .WithAlias("label") + .WithName("Label") + .WithDataTypeId(Constants.DataTypes.LabelString) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label) + .WithVariations(ContentVariation.Nothing) + .Done(); foreach (var template in templates) { @@ -81,22 +86,23 @@ public abstract class ContentEditingServiceTestsBase : UmbracoIntegrationTestWit .WithName("Culture Variation Test") .WithContentVariation(ContentVariation.Culture) .AddPropertyType() - .WithAlias("variantTitle") - .WithName("Variant Title") - .WithVariations(ContentVariation.Culture) - .Done() + .WithAlias("variantTitle") + .WithName("Variant Title") + .WithMandatory(true) + .WithVariations(ContentVariation.Culture) + .Done() .AddPropertyType() - .WithAlias("invariantTitle") - .WithName("Invariant Title") - .WithVariations(ContentVariation.Nothing) - .Done() + .WithAlias("invariantTitle") + .WithName("Invariant Title") + .WithVariations(ContentVariation.Nothing) + .Done() .AddPropertyType() - .WithAlias("variantLabel") - .WithName("Variant Label") - .WithDataTypeId(Constants.DataTypes.LabelString) - .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label) - .WithVariations(ContentVariation.Culture) - .Done() + .WithAlias("variantLabel") + .WithName("Variant Label") + .WithDataTypeId(Constants.DataTypes.LabelString) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label) + .WithVariations(ContentVariation.Culture) + .Done() .Build(); contentType.AllowedAsRoot = true; ContentTypeService.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index db1e6d27e1..30521afe57 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -1,4 +1,4 @@ - + true Umbraco.Cms.Tests.Integration @@ -97,6 +97,9 @@ ContentEditingServiceTests.cs + + ContentEditingServiceTests.cs + ContentPublishingServiceTests.cs