From 012b43a1c2b0f1708d645d232c6cd4f765db5f39 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 22 Nov 2023 12:52:08 +0100 Subject: [PATCH] Publishing in the Management API (#14774) * make CoreScopeProvider available for derived classes * Create publish controller * Add publish functionality * Remove unneeded using * Implement publish for multiple cultures * support multiple cultures in controler * Dont validate properties * Refactor to use PublishingOperationStatus * refactor to use proper publish async methods * Refactor publish logic into own service * Commit some demo code * Add notes about what errors can happen when publishing * Rework ContentPublishingService and introduce explicit Publish and PublishBranch methods in ContentService * Fix merge * Allow the publishing strategy to do its job * Improved check for unsaved changes * Make the old content controller work (as best possible) * Remove SaveAndPublish (SaveAndPublishBranch) from all tests * Proper guards for invalid cultures when publishing * Fix edge cases for property validation and content unpublishing + add unpublishing to ContentPublishingService * Clear out a few TODOs - we'll accept the behavior for now * Unpublish controller * Fix merge * Fix branch publish notifications * Added extra test for publishing unpublished cultures and added FIXME comments for when we fix the state of published cultures in content --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Zeegaan --- .../Content/ContentControllerBase.cs | 63 ++- .../Document/PublishDocumentController.cs | 38 ++ ...ublishDocumentWithDescendantsController.cs | 43 ++ .../Document/UnpublishDocumentController.cs | 38 ++ .../Document/PublishDocumentRequestModel.cs | 6 + ...lishDocumentWithDescendantsRequestModel.cs | 6 + .../Document/UnpublishDocumentRequestModel.cs | 6 + .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Services/ContentEditingServiceBase.cs | 11 +- .../Services/ContentPublishingService.cs | 114 ++++ src/Umbraco.Core/Services/ContentService.cs | 278 ++++------ .../Services/IContentPublishingService.cs | 34 ++ src/Umbraco.Core/Services/IContentService.cs | 65 +-- .../ContentPublishingOperationStatus.cs | 21 + .../Services/PropertyValidationService.cs | 10 +- src/Umbraco.Core/Services/PublishResult.cs | 4 +- .../Services/PublishResultType.cs | 7 +- .../Controllers/ContentController.cs | 59 +- tests/Umbraco.TestData/LoadTestController.cs | 6 +- .../UmbracoTestDataController.cs | 2 +- .../Services/ContentServiceTests.cs | 375 +++++++++---- .../ContentVariantAllowedActionTests.cs | 3 +- .../DocumentVersionRepositoryTest.cs | 33 +- .../Scoping/ScopedNuCacheTests.cs | 6 +- .../Services/ContentEventsTests.cs | 182 ++++--- .../ContentPublishingServiceTests.Publish.cs | 503 ++++++++++++++++++ ...ContentPublishingServiceTests.Unpublish.cs | 184 +++++++ .../Services/ContentPublishingServiceTests.cs | 120 +++++ .../ContentServiceNotificationTests.cs | 46 +- .../ContentServicePublishBranchTests.cs | 68 ++- .../Services/ContentServiceTagsTests.cs | 87 +-- .../Services/ContentTypeServiceTests.cs | 16 +- .../ContentTypeServiceVariantsTests.cs | 15 +- .../ContentVersionCleanupServiceTest.cs | 5 +- .../Services/NuCacheRebuildTests.cs | 6 +- .../Services/TagServiceTests.cs | 21 +- .../Umbraco.Tests.Integration.csproj | 6 + .../Controllers/ContentControllerTests.cs | 30 +- .../Controllers/EntityControllerTests.cs | 24 +- .../UrlAndDomains/DomainAndUrlsTests.cs | 4 +- .../Umbraco.Core/Models/VariationTests.cs | 38 ++ 41 files changed, 2017 insertions(+), 567 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/UnpublishDocumentRequestModel.cs create mode 100644 src/Umbraco.Core/Services/ContentPublishingService.cs create mode 100644 src/Umbraco.Core/Services/IContentPublishingService.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs index 4ddf87f045..e2accb7273 100644 --- a/src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Content/ContentControllerBase.cs @@ -16,7 +16,7 @@ public class ContentControllerBase : ManagementApiControllerBase .WithDetail("A notification handler prevented the content operation.") .Build()), ContentEditingOperationStatus.ContentTypeNotFound => NotFound(new ProblemDetailsBuilder() - .WithTitle("Cancelled by notification") + .WithTitle("The requested content could not be found") .Build()), ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch => BadRequest(new ProblemDetailsBuilder() .WithTitle("Content type culture variance mismatch") @@ -66,6 +66,67 @@ public class ContentControllerBase : ManagementApiControllerBase .Build()), }; + protected IActionResult ContentPublishingOperationStatusResult(ContentPublishingOperationStatus status) => + status switch + { + ContentPublishingOperationStatus.ContentNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("The requested content could not be found") + .Build()), + ContentPublishingOperationStatus.CancelledByEvent => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Publish cancelled by event") + .WithDetail("The publish operation was cancelled by an event.") + .Build()), + ContentPublishingOperationStatus.ContentInvalid => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Invalid content") + .WithDetail("The specified content had an invalid configuration.") + .Build()), + ContentPublishingOperationStatus.NothingToPublish => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Nothing to publish") + .WithDetail("None of the specified cultures needed publishing.") + .Build()), + ContentPublishingOperationStatus.MandatoryCultureMissing => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Mandatory culture missing") + .WithDetail("Must include all mandatory cultures when publishing.") + .Build()), + ContentPublishingOperationStatus.HasExpired => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Content expired") + .WithDetail("Could not publish the content because it was expired.") + .Build()), + ContentPublishingOperationStatus.CultureHasExpired => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Content culture expired") + .WithDetail("Could not publish the content because some of the specified cultures were expired.") + .Build()), + ContentPublishingOperationStatus.AwaitingRelease => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Content awaiting release") + .WithDetail("Could not publish the content because it was awaiting release.") + .Build()), + ContentPublishingOperationStatus.CultureAwaitingRelease => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Content culture awaiting release") + .WithDetail("Could not publish the content because some of the specified cultures were awaiting release.") + .Build()), + ContentPublishingOperationStatus.InTrash => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Content in the recycle bin") + .WithDetail("Could not publish the content because it was in the recycle bin.") + .Build()), + ContentPublishingOperationStatus.PathNotPublished => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Parent not published") + .WithDetail("Could not publish the content because its parent was not published.") + .Build()), + ContentPublishingOperationStatus.ConcurrencyViolation => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Concurrency violation detected") + .WithDetail("An attempt was made to publish a version older than the latest version.") + .Build()), + ContentPublishingOperationStatus.UnsavedChanges => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Unsaved changes") + .WithDetail("Could not publish the content because it had unsaved changes. Make sure to save all changes before attempting a publish.") + .Build()), + ContentPublishingOperationStatus.Failed => BadRequest(new ProblemDetailsBuilder() + .WithTitle("Publish or unpublish failed") + .WithDetail("An unspecified error occurred while (un)publishing. Please check the logs for additional information.") + .Build()), + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."), + }; + protected IActionResult ContentCreatingOperationStatusResult(ContentCreatingOperationStatus status) => status switch { diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs new file mode 100644 index 0000000000..99b5306951 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentController.cs @@ -0,0 +1,38 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +public class PublishDocumentController : DocumentControllerBase +{ + private readonly IContentPublishingService _contentPublishingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public PublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _contentPublishingService = contentPublishingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{id:guid}/publish")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Publish(Guid id, PublishDocumentRequestModel requestModel) + { + Attempt attempt = await _contentPublishingService.PublishAsync( + id, + requestModel.Cultures, + CurrentUserKey(_backOfficeSecurityAccessor)); + return attempt.Success + ? Ok() + : ContentPublishingOperationStatusResult(attempt.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs new file mode 100644 index 0000000000..5b75167e57 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs @@ -0,0 +1,43 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +public class PublishDocumentWithDescendantsController : DocumentControllerBase +{ + private readonly IContentPublishingService _contentPublishingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public PublishDocumentWithDescendantsController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _contentPublishingService = contentPublishingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{id:guid}/publish-with-descendants")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task PublishWithDescendants(Guid id, PublishDocumentWithDescendantsRequestModel requestModel) + { + Attempt> attempt = await _contentPublishingService.PublishBranchAsync( + id, + requestModel.Cultures, + requestModel.IncludeUnpublishedDescendants, + CurrentUserKey(_backOfficeSecurityAccessor)); + + // FIXME: when we get to implement proper validation handling, this should return a collection of status codes by key (based on attempt.Result) + return attempt.Success + ? Ok() + : ContentPublishingOperationStatusResult( + attempt.Result?.Values.FirstOrDefault(r => r is not ContentPublishingOperationStatus.Success) + ?? throw new NotSupportedException("The attempt was not successful - at least one result value should be unsuccessful too")); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs new file mode 100644 index 0000000000..6a3fd42425 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/UnpublishDocumentController.cs @@ -0,0 +1,38 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +public class UnpublishDocumentController : DocumentControllerBase +{ + private readonly IContentPublishingService _contentPublishingService; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UnpublishDocumentController(IContentPublishingService contentPublishingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _contentPublishingService = contentPublishingService; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + [HttpPut("{id:guid}/unpublish")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task Unpublish(Guid id, UnpublishDocumentRequestModel requestModel) + { + Attempt attempt = await _contentPublishingService.UnpublishAsync( + id, + requestModel.Culture, + CurrentUserKey(_backOfficeSecurityAccessor)); + return attempt.Success + ? Ok() + : ContentPublishingOperationStatusResult(attempt.Result); + } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs new file mode 100644 index 0000000000..1f98124822 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class PublishDocumentRequestModel +{ + public required IEnumerable Cultures { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs new file mode 100644 index 0000000000..f83f7c28d1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/PublishDocumentWithDescendantsRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class PublishDocumentWithDescendantsRequestModel : PublishDocumentRequestModel +{ + public bool IncludeUnpublishedDescendants { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/UnpublishDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/UnpublishDocumentRequestModel.cs new file mode 100644 index 0000000000..199b7ff2c0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/UnpublishDocumentRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class UnpublishDocumentRequestModel +{ + public string? Culture { get; set; } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index b72dd4adc8..f47d813839 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -301,6 +301,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 2907a1b89f..4aa4246a96 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -18,7 +18,6 @@ internal abstract class ContentEditingServiceBase> _logger; - private readonly ICoreScopeProvider _scopeProvider; private readonly ITreeEntitySortingService _treeEntitySortingService; private readonly IUserIdKeyResolver _userIdKeyResolver; @@ -35,9 +34,9 @@ internal abstract class ContentEditingServiceBase items, int userId); + protected ICoreScopeProvider CoreScopeProvider { get; } + protected TContentService ContentService { get; } protected TContentTypeService ContentTypeService { get; } @@ -110,7 +111,7 @@ internal abstract class ContentEditingServiceBase> HandleDeletionAsync(Guid key, Guid userKey, bool mustBeTrashed, Func performDelete) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(); + using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); TContent? content = ContentService.GetById(key); if (content == null) { @@ -135,7 +136,7 @@ internal abstract class ContentEditingServiceBase> HandleMoveAsync(Guid key, Guid? parentKey, Guid userKey) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(); + using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); TContent? content = ContentService.GetById(key); if (content is null) { @@ -172,7 +173,7 @@ internal abstract class ContentEditingServiceBase> HandleCopyAsync(Guid key, Guid? parentKey, bool relateToOriginal, bool includeDescendants, Guid userKey) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(); + using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); TContent? content = ContentService.GetById(key); if (content is null) { diff --git a/src/Umbraco.Core/Services/ContentPublishingService.cs b/src/Umbraco.Core/Services/ContentPublishingService.cs new file mode 100644 index 0000000000..a3f8da6748 --- /dev/null +++ b/src/Umbraco.Core/Services/ContentPublishingService.cs @@ -0,0 +1,114 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public class ContentPublishingService : IContentPublishingService +{ + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IContentService _contentService; + private readonly IUserIdKeyResolver _userIdKeyResolver; + + public ContentPublishingService(ICoreScopeProvider coreScopeProvider, IContentService contentService, IUserIdKeyResolver userIdKeyResolver) + { + _coreScopeProvider = coreScopeProvider; + _contentService = contentService; + _userIdKeyResolver = userIdKeyResolver; + } + + /// + public async Task> PublishAsync(Guid key, IEnumerable cultures, Guid userKey) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + IContent? content = _contentService.GetById(key); + if (content is null) + { + return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound); + } + + var userId = await _userIdKeyResolver.GetAsync(userKey); + PublishResult result = _contentService.Publish(content, cultures.ToArray(), userId); + scope.Complete(); + + ContentPublishingOperationStatus contentPublishingOperationStatus = ToContentPublishingOperationStatus(result); + return contentPublishingOperationStatus is ContentPublishingOperationStatus.Success + ? Attempt.Succeed(ToContentPublishingOperationStatus(result)) + : Attempt.Fail(ToContentPublishingOperationStatus(result)); + } + + /// + public async Task>> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + IContent? content = _contentService.GetById(key); + if (content is null) + { + var payload = new Dictionary + { + { key, ContentPublishingOperationStatus.ContentNotFound }, + }; + + return Attempt>.Fail(payload); + } + + var userId = await _userIdKeyResolver.GetAsync(userKey); + IEnumerable result = _contentService.PublishBranch(content, force, cultures.ToArray(), userId); + scope.Complete(); + + var payloads = result.ToDictionary(r => r.Content.Key, ToContentPublishingOperationStatus); + return payloads.All(p => p.Value is ContentPublishingOperationStatus.Success) + ? Attempt>.Succeed(payloads) + : Attempt>.Fail(payloads); + } + + /// + public async Task> UnpublishAsync(Guid key, string? culture, Guid userKey) + { + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(); + IContent? content = _contentService.GetById(key); + if (content is null) + { + return Attempt.Fail(ContentPublishingOperationStatus.ContentNotFound); + } + + var userId = await _userIdKeyResolver.GetAsync(userKey); + PublishResult result = _contentService.Unpublish(content, culture ?? "*", userId); + scope.Complete(); + + ContentPublishingOperationStatus contentPublishingOperationStatus = ToContentPublishingOperationStatus(result); + return contentPublishingOperationStatus is ContentPublishingOperationStatus.Success + ? Attempt.Succeed(ToContentPublishingOperationStatus(result)) + : Attempt.Fail(ToContentPublishingOperationStatus(result)); + } + + private static ContentPublishingOperationStatus ToContentPublishingOperationStatus(PublishResult publishResult) + => publishResult.Result switch + { + PublishResultType.SuccessPublish => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessPublishCulture => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessPublishAlready => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessUnpublish => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessUnpublishAlready => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessUnpublishCulture => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessUnpublishMandatoryCulture => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessUnpublishLastCulture => ContentPublishingOperationStatus.Success, + PublishResultType.SuccessMixedCulture => ContentPublishingOperationStatus.Success, + // PublishResultType.FailedPublish => expr, <-- never used directly in a PublishResult + PublishResultType.FailedPublishPathNotPublished => ContentPublishingOperationStatus.PathNotPublished, + PublishResultType.FailedPublishHasExpired => ContentPublishingOperationStatus.HasExpired, + PublishResultType.FailedPublishAwaitingRelease => ContentPublishingOperationStatus.AwaitingRelease, + PublishResultType.FailedPublishCultureHasExpired => ContentPublishingOperationStatus.CultureHasExpired, + PublishResultType.FailedPublishCultureAwaitingRelease => ContentPublishingOperationStatus.CultureAwaitingRelease, + PublishResultType.FailedPublishIsTrashed => ContentPublishingOperationStatus.InTrash, + PublishResultType.FailedPublishCancelledByEvent => ContentPublishingOperationStatus.CancelledByEvent, + PublishResultType.FailedPublishContentInvalid => ContentPublishingOperationStatus.ContentInvalid, + PublishResultType.FailedPublishNothingToPublish => ContentPublishingOperationStatus.NothingToPublish, + PublishResultType.FailedPublishMandatoryCultureMissing => ContentPublishingOperationStatus.MandatoryCultureMissing, + PublishResultType.FailedPublishConcurrencyViolation => ContentPublishingOperationStatus.ConcurrencyViolation, + PublishResultType.FailedPublishUnsavedChanges => ContentPublishingOperationStatus.UnsavedChanges, + PublishResultType.FailedUnpublish => ContentPublishingOperationStatus.Failed, + PublishResultType.FailedUnpublishCancelledByEvent => ContentPublishingOperationStatus.CancelledByEvent, + _ => throw new ArgumentOutOfRangeException() + }; +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index b870ebbb02..da0d5d6ad6 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1110,10 +1110,41 @@ public class ContentService : RepositoryService, IContentService return OperationResult.Succeed(eventMessages); } - /// + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(Publish)} instead. Will be removed in V16")] public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId) + => Publish(content, new[] { culture }, userId); + + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(Publish)} instead. Will be removed in V16")] + public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId) + => Publish(content, cultures, userId); + + public PublishResult Publish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId) { - EventMessages evtMsgs = EventMessagesFactory.Get(); + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + if (cultures is null) + { + throw new ArgumentNullException(nameof(cultures)); + } + + if (cultures.Any(c => c.IsNullOrWhiteSpace()) || cultures.Distinct().Count() != cultures.Length) + { + throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures)); + } + + // we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications + if (HasUnsavedChanges(content)) + { + return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, EventMessagesFactory.Get(), content); + } + + if (content.Name != null && content.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } PublishedState publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) @@ -1126,71 +1157,22 @@ public class ContentService : RepositoryService, IContentService // cannot accept a specific culture for invariant content type (but '*' is ok) if (content.ContentType.VariesByCulture()) { - if (culture.IsNullOrWhiteSpace()) + if (cultures.Length > 1 && cultures.Contains("*")) { - throw new NotSupportedException("Invariant culture is not supported by variant content types."); + throw new ArgumentException("Cannot combine wildcard and specific cultures when publishing variant content types.", nameof(cultures)); } } else { - if (!culture.IsNullOrWhiteSpace() && culture != "*") + if (cultures.Length == 0) { - throw new NotSupportedException( - $"Culture \"{culture}\" is not supported by invariant content types."); - } - } - - if (content.Name != null && content.Name.Length > 255) - { - throw new InvalidOperationException("Name cannot be more than 255 characters in length."); - } - - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var allLangs = _languageRepository.GetMany().ToList(); - - // Change state to publishing - content.PublishedState = PublishedState.Publishing; - var savingNotification = new ContentSavingNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); + cultures = new[] { "*" }; } - // if culture is specific, first publish the invariant values, then publish the culture itself. - // if culture is '*', then publish them all (including variants) - - // this will create the correct culture impact even if culture is * or null - var impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content); - - // publish the culture(s) - // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. - content.PublishCulture(impact); - - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); - scope.Complete(); - return result; - } - } - - /// - public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId) - { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (cultures == null) - { - throw new ArgumentNullException(nameof(cultures)); - } - - if (content.Name != null && content.Name.Length > 255) - { - throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + if (cultures[0] != "*" || cultures.Length > 1) + { + throw new ArgumentException($"Only wildcard culture is supported when publishing invariant content types.", nameof(cultures)); + } } using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -1201,37 +1183,18 @@ public class ContentService : RepositoryService, IContentService EventMessages evtMsgs = EventMessagesFactory.Get(); - var savingNotification = new ContentSavingNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - - var varies = content.ContentType.VariesByCulture(); - - if (cultures.Length == 0 && !varies) - { - // No cultures specified and doesn't vary, so publish it, else nothing to publish - return SaveAndPublish(content, userId: userId); - } - - if (cultures.Any(x => x == null || x == "*")) - { - throw new InvalidOperationException( - "Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); - } - - IEnumerable impacts = - cultures.Select(x => _cultureImpactFactory.ImpactExplicit(x, IsDefaultCulture(allLangs, x))); + // this will create the correct culture impact even if culture is * or null + IEnumerable impacts = + cultures.Select(culture => _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content)); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. - foreach (CultureImpact impact in impacts) + foreach (CultureImpact? impact in impacts) { content.PublishCulture(impact); } - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); scope.Complete(); return result; } @@ -1286,12 +1249,6 @@ public class ContentService : RepositoryService, IContentService var allLangs = _languageRepository.GetMany().ToList(); - var savingNotification = new ContentSavingNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { @@ -1300,7 +1257,7 @@ public class ContentService : RepositoryService, IContentService // because we don't want to actually unpublish every culture and then the document, we just want everything // to be non-routable so that when it's re-published all variants were as they were. content.PublishedState = PublishedState.Unpublishing; - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); scope.Complete(); return result; } @@ -1314,7 +1271,7 @@ public class ContentService : RepositoryService, IContentService var removed = content.UnpublishCulture(culture); // Save and publish any changes - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); scope.Complete(); @@ -1332,7 +1289,7 @@ public class ContentService : RepositoryService, IContentService } /// - /// Saves a document and publishes/unpublishes any pending publishing changes made to the document. + /// Publishes/unpublishes any pending publishing changes made to the document. /// /// /// @@ -1365,15 +1322,9 @@ public class ContentService : RepositoryService, IContentService scope.WriteLock(Constants.Locks.ContentTree); - var savingNotification = new ContentSavingNotification(content, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - } - var allLangs = _languageRepository.GetMany().ToList(); - PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, savingNotification.State, userId); + PublishResult result = CommitDocumentChangesInternal(scope, content, evtMsgs, allLangs, userId, out _); scope.Complete(); return result; } @@ -1385,7 +1336,6 @@ public class ContentService : RepositoryService, IContentService /// /// /// - /// /// /// /// @@ -1404,8 +1354,8 @@ public class ContentService : RepositoryService, IContentService IContent content, EventMessages eventMessages, IReadOnlyCollection allLangs, - IDictionary? notificationState, - int userId = Constants.Security.SuperUserId, + int userId, + out IDictionary? initialNotificationState, bool branchOne = false, bool branchRoot = false) { @@ -1467,6 +1417,7 @@ public class ContentService : RepositoryService, IContentService _documentRepository.Save(c); } + initialNotificationState = null; if (publishing) { // Determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo @@ -1484,7 +1435,7 @@ public class ContentService : RepositoryService, IContentService culturesUnpublishing, eventMessages, allLangs, - notificationState); + out initialNotificationState); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! @@ -1547,7 +1498,7 @@ public class ContentService : RepositoryService, IContentService // handling events, business rules, etc // note: StrategyUnpublish flips the PublishedState to Unpublishing! // note: This unpublishes the entire document (not different variants) - unpublishResult = StrategyCanUnpublish(scope, content, eventMessages, notificationState); + unpublishResult = StrategyCanUnpublish(scope, content, eventMessages, out initialNotificationState); if (unpublishResult.Success) { unpublishResult = StrategyUnpublish(content, eventMessages); @@ -1560,6 +1511,7 @@ public class ContentService : RepositoryService, IContentService // PublishState to anything other than Publishing or Unpublishing - which is precisely // what we want to do here - throws content.Published = content.Published; + return unpublishResult; } } else @@ -1574,10 +1526,6 @@ public class ContentService : RepositoryService, IContentService // Persist the document SaveDocument(content); - // raise the Saved event, always - scope.Notifications.Publish( - new ContentSavedNotification(content, eventMessages).WithState(notificationState)); - // we have tried to unpublish - won't happen in a branch if (unpublishing) { @@ -1586,7 +1534,7 @@ public class ContentService : RepositoryService, IContentService { // events and audit scope.Notifications.Publish( - new ContentUnpublishedNotification(content, eventMessages).WithState(notificationState)); + new ContentUnpublishedNotification(content, eventMessages).WithState(initialNotificationState)); scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshBranch, eventMessages)); if (culturesUnpublishing != null) @@ -1648,7 +1596,7 @@ public class ContentService : RepositoryService, IContentService scope.Notifications.Publish( new ContentTreeChangeNotification(content, changeType, eventMessages)); scope.Notifications.Publish( - new ContentPublishedNotification(content, eventMessages).WithState(notificationState)); + new ContentPublishedNotification(content, eventMessages).WithState(initialNotificationState)); } // it was not published and now is... descendants that were 'published' (but @@ -1658,7 +1606,7 @@ public class ContentService : RepositoryService, IContentService { IContent[] descendants = GetPublishedDescendantsLocked(content).ToArray(); scope.Notifications.Publish( - new ContentPublishedNotification(descendants, eventMessages).WithState(notificationState)); + new ContentPublishedNotification(descendants, eventMessages).WithState(initialNotificationState)); } switch (publishResult.Result) @@ -1758,13 +1706,6 @@ public class ContentService : RepositoryService, IContentService continue; // shouldn't happen but no point in processing this document if there's nothing there } - var savingNotification = new ContentSavingNotification(d, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; - } - foreach (var c in pendingCultures) { // Clear this schedule for this culture @@ -1775,7 +1716,7 @@ public class ContentService : RepositoryService, IContentService } _documentRepository.PersistContentSchedule(d, contentSchedule); - PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); + PublishResult result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, d.WriterId, out _); if (result.Success == false) { _logger.LogError(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1830,13 +1771,6 @@ public class ContentService : RepositoryService, IContentService continue; // shouldn't happen but no point in processing this document if there's nothing there } - var savingNotification = new ContentSavingNotification(d, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; - } - var publishing = true; foreach (var culture in pendingCultures) { @@ -1881,7 +1815,7 @@ public class ContentService : RepositoryService, IContentService else { _documentRepository.PersistContentSchedule(d, contentSchedule); - result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, savingNotification.State, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, evtMsgs, allLangs.Value, d.WriterId, out _); } if (result.Success == false) @@ -1905,7 +1839,7 @@ public class ContentService : RepositoryService, IContentService else { _documentRepository.PersistContentSchedule(d, contentSchedule); - result = SaveAndPublish(d, userId: d.WriterId); + result = Publish(d, d.AvailableCultures.ToArray(), userId: d.WriterId); } if (result.Success == false) @@ -1924,10 +1858,8 @@ public class ContentService : RepositoryService, IContentService } // utility 'PublishCultures' func used by SaveAndPublishBranch - private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) + private bool PublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) { - // TODO: Th is does not support being able to return invalid property details to bubble up to the UI - // variant content type - publish specified cultures // invariant content type - publish only the invariant culture if (content.ContentType.VariesByCulture()) @@ -1945,7 +1877,7 @@ public class ContentService : RepositoryService, IContentService } // utility 'ShouldPublish' func used by SaveAndPublishBranch - private HashSet? SaveAndPublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force) + private HashSet? PublishBranch_ShouldPublish(ref HashSet? cultures, string c, bool published, bool edited, bool isRoot, bool force) { // if published, republish if (published) @@ -1978,7 +1910,7 @@ public class ContentService : RepositoryService, IContentService return cultures; } - /// + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")] public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() @@ -1998,13 +1930,13 @@ public class ContentService : RepositoryService, IContentService // invariant content type if (!c.ContentType.VariesByCulture()) { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); } // variant content type, specific culture if (culture != "*") { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force); } // variant content type, all cultures @@ -2014,7 +1946,7 @@ public class ContentService : RepositoryService, IContentService // others will have to 'republish this culture' foreach (var x in c.AvailableCultures) { - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); } return culturesToPublish; @@ -2026,16 +1958,25 @@ public class ContentService : RepositoryService, IContentService : null; // null means 'nothing to do' } - return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); + return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId); } - /// + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")] public IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId) + => PublishBranch(content, force, cultures, userId); + + /// + public IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId) { // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() // and not to == them, else we would be comparing references, and that is a bad thing cultures = cultures ?? Array.Empty(); + if (content.ContentType.VariesByCulture() is false && cultures.Length == 0) + { + cultures = new[] { "*" }; + } + // determines cultures to be published // can be: null (content is not impacted), an empty set (content is impacted but already published), or cultures HashSet? ShouldPublish(IContent c) @@ -2046,7 +1987,7 @@ public class ContentService : RepositoryService, IContentService // invariant content type if (!c.ContentType.VariesByCulture()) { - return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); + return PublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force); } // variant content type, specific cultures @@ -2056,7 +1997,7 @@ public class ContentService : RepositoryService, IContentService // others will have to 'republish this culture' foreach (var x in cultures) { - SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); + PublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force); } return culturesToPublish; @@ -2068,10 +2009,10 @@ public class ContentService : RepositoryService, IContentService : null; // null means 'nothing to do' } - return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId); + return PublishBranch(content, force, ShouldPublish, PublishBranch_PublishCultures, userId); } - internal IEnumerable SaveAndPublishBranch( + internal IEnumerable PublishBranch( IContent document, bool force, Func?> shouldPublish, @@ -2092,6 +2033,7 @@ public class ContentService : RepositoryService, IContentService var results = new List(); var publishedDocuments = new List(); + IDictionary? initialNotificationState = null; using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { scope.WriteLock(Constants.Locks.ContentTree); @@ -2110,7 +2052,7 @@ public class ContentService : RepositoryService, IContentService } // deal with the branch root - if it fails, abort - PublishResult? result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out IDictionary notificationState); + PublishResult? result = PublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, eventMessages, userId, allLangs, out initialNotificationState); if (result != null) { results.Add(result); @@ -2145,7 +2087,7 @@ public class ContentService : RepositoryService, IContentService } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _); + result = PublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, eventMessages, userId, allLangs, out _); if (result != null) { results.Add(result); @@ -2169,7 +2111,7 @@ public class ContentService : RepositoryService, IContentService // (SaveAndPublishBranchOne does *not* do it) scope.Notifications.Publish( new ContentTreeChangeNotification(document, TreeChangeTypes.RefreshBranch, eventMessages)); - scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState)); + scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(initialNotificationState)); scope.Complete(); } @@ -2180,7 +2122,7 @@ public class ContentService : RepositoryService, IContentService // shouldPublish: a function determining whether the document has changes that need to be published // note - 'force' is handled by 'editing' // publishValues: a function publishing values (using the appropriate PublishCulture calls) - private PublishResult? SaveAndPublishBranchItem( + private PublishResult? PublishBranchItem( ICoreScope scope, IContent document, Func?> shouldPublish, @@ -2191,11 +2133,18 @@ public class ContentService : RepositoryService, IContentService EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs, - out IDictionary notificationState) + out IDictionary? initialNotificationState) { - notificationState = new Dictionary(); HashSet? culturesToPublish = shouldPublish(document); + initialNotificationState = null; + + // we need to guard against unsaved changes before proceeding; the document will be saved, but we're not firing any saved notifications + if (HasUnsavedChanges(document)) + { + return new PublishResult(PublishResultType.FailedPublishUnsavedChanges, evtMsgs, document); + } + // null = do not include if (culturesToPublish == null) { @@ -2208,12 +2157,6 @@ public class ContentService : RepositoryService, IContentService return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document); } - var savingNotification = new ContentSavingNotification(document, evtMsgs); - if (scope.Notifications.PublishCancelable(savingNotification)) - { - return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); - } - // publish & check if values are valid if (!publishCultures(document, culturesToPublish, allLangs)) { @@ -2221,11 +2164,10 @@ public class ContentService : RepositoryService, IContentService return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, savingNotification.State, userId, true, isRoot); + PublishResult result = CommitDocumentChangesInternal(scope, document, evtMsgs, allLangs, userId, out initialNotificationState, true, isRoot); if (result.Success) { publishedDocuments.Add(document); - notificationState = savingNotification.State; } return result; @@ -2967,6 +2909,8 @@ public class ContentService : RepositoryService, IContentService return OperationResult.Succeed(eventMessages); } + private bool HasUnsavedChanges(IContent content) => content.HasIdentity is false || content.IsDirty(); + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { using (ICoreScope scope = ScopeProvider.CreateCoreScope()) @@ -3057,7 +3001,6 @@ public class ContentService : RepositoryService, IContentService /// /// /// - /// /// private PublishResult StrategyCanPublish( ICoreScope scope, @@ -3067,11 +3010,14 @@ public class ContentService : RepositoryService, IContentService IReadOnlyCollection? culturesUnpublishing, EventMessages evtMsgs, IReadOnlyCollection allLangs, - IDictionary? notificationState) + out IDictionary? initialNotificationState) { // raise Publishing notification - if (scope.Notifications.PublishCancelable( - new ContentPublishingNotification(content, evtMsgs).WithState(notificationState))) + var notification = new ContentPublishingNotification(content, evtMsgs); + var notificationResult = scope.Notifications.PublishCancelable(notification); + initialNotificationState = notification.State; + + if (notificationResult) { _logger.LogInformation("Document {ContentName} (id={ContentId}) cannot be published: {Reason}", content.Name, content.Id, "publishing was cancelled"); return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -3310,10 +3256,18 @@ public class ContentService : RepositoryService, IContentService /// /// /// - private PublishResult StrategyCanUnpublish(ICoreScope scope, IContent content, EventMessages evtMsgs, IDictionary? notificationState) + private PublishResult StrategyCanUnpublish( + ICoreScope scope, + IContent content, + EventMessages evtMsgs, + out IDictionary? initialNotificationState) { // raise Unpublishing notification - if (scope.Notifications.PublishCancelable(new ContentUnpublishingNotification(content, evtMsgs).WithState(notificationState))) + var notification = new ContentUnpublishingNotification(content, evtMsgs); + var notificationResult = scope.Notifications.PublishCancelable(notification); + initialNotificationState = notification.State; + + if (notificationResult) { _logger.LogInformation( "Document {ContentName} (id={ContentId}) cannot be unpublished: unpublishing was cancelled.", content.Name, content.Id); diff --git a/src/Umbraco.Core/Services/IContentPublishingService.cs b/src/Umbraco.Core/Services/IContentPublishingService.cs new file mode 100644 index 0000000000..6218a38bc8 --- /dev/null +++ b/src/Umbraco.Core/Services/IContentPublishingService.cs @@ -0,0 +1,34 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IContentPublishingService +{ + /// + /// Publishes a single content item. + /// + /// The key of the root content. + /// The cultures to publish. + /// The identifier of the user performing the operation. + /// Status of the publish operation. + Task> PublishAsync(Guid key, IEnumerable cultures, Guid userKey); + + /// + /// Publishes a content branch. + /// + /// The key of the root content. + /// The cultures to publish. + /// A value indicating whether to force-publish content that is not already published. + /// The identifier of the user performing the operation. + /// A dictionary of attempted content item keys and their corresponding publishing status. + Task>> PublishBranchAsync(Guid key, IEnumerable cultures, bool force, Guid userKey); + + /// + /// Unpublishes a single content item. + /// + /// The key of the root content. + /// The culture to unpublish. Use null to unpublish all cultures. + /// The identifier of the user performing the operation. + /// Status of the publish operation. + Task> UnpublishAsync(Guid key, string? culture, Guid userKey); +} diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index e4c9f90616..9ca9940a71 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -361,64 +361,33 @@ public interface IContentService : IContentServiceBase #region Publish Document - /// - /// Saves and publishes a document. - /// - /// - /// - /// By default, publishes all variations of the document, but it is possible to specify a culture to be - /// published. - /// - /// When a culture is being published, it includes all varying values along with all invariant values. - /// The document is *always* saved, even when publishing fails. - /// - /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor - /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. - /// - /// - /// The document to publish. - /// The culture to publish. - /// The identifier of the user performing the action. + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(Publish)} instead. Will be removed in V16")] PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = Constants.Security.SuperUserId); - /// - /// Saves and publishes a document. - /// - /// - /// - /// By default, publishes all variations of the document, but it is possible to specify a culture to be - /// published. - /// - /// When a culture is being published, it includes all varying values along with all invariant values. - /// The document is *always* saved, even when publishing fails. - /// - /// The document to publish. - /// The cultures to publish. - /// The identifier of the user performing the action. + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(Publish)} instead. Will be removed in V16")] PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId); /// - /// Saves and publishes a document branch. + /// Publishes a document. /// - /// The root document. - /// A value indicating whether to force-publish documents that are not already published. - /// A culture, or "*" for all cultures. - /// The identifier of the user performing the operation. /// - /// - /// Unless specified, all cultures are re-published. Otherwise, one culture can be specified. To act on more - /// than one culture, see the other overloads of this method. - /// - /// - /// The parameter determines which documents are published. When false, - /// only those documents that are already published, are republished. When true, all documents are - /// published. The root of the branch is always published, regardless of . - /// + /// When a culture is being published, it includes all varying values along with all invariant values. + /// Wildcards (*) can be used as culture identifier to publish all cultures. + /// An empty array (or a wildcard) can be passed for culture invariant content. /// + /// The document to publish. + /// The cultures to publish. + /// The identifier of the user performing the action. + PublishResult Publish(IContent content, string[] cultures, int userId = Constants.Security.SuperUserId); + + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")] IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId); + [Obsolete($"This method no longer saves content, only publishes it. Please use {nameof(PublishBranch)} instead. Will be removed in V16")] + IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId); + /// - /// Saves and publishes a document branch. + /// Publishes a document branch. /// /// The root document. /// A value indicating whether to force-publish documents that are not already published. @@ -431,7 +400,7 @@ public interface IContentService : IContentServiceBase /// published. The root of the branch is always published, regardless of . /// /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId); + IEnumerable PublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId); ///// ///// Saves and publishes a document branch. diff --git a/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs new file mode 100644 index 0000000000..f72d04f018 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/ContentPublishingOperationStatus.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum ContentPublishingOperationStatus +{ + Success, + ContentNotFound, + CancelledByEvent, + ContentInvalid, + NothingToPublish, + MandatoryCultureMissing, + HasExpired, + CultureHasExpired, + AwaitingRelease, + CultureAwaitingRelease, + InTrash, + PathNotPublished, + ConcurrencyViolation, + UnsavedChanges, + Failed, // unspecified failure (can happen on unpublish at the time of writing) + Unknown +} diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index a3e0e63910..b9fc4afd30 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -179,14 +179,20 @@ public class PropertyValidationService : IPropertyValidationService } // else validate vvalues (but don't revalidate pvalue) - var pvalues = property.Values.Where(x => + var vvalues = property.Values.Where(x => x != pvalue && // don't revalidate pvalue property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok (culture == "*" || x.Culture.InvariantEquals(culture)) && // the culture matches (segment == "*" || x.Segment.InvariantEquals(segment))) // the segment matches .ToList(); - return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); + // if we do not have any vvalues at this point, validate null (no variant values present) + if (vvalues.Any() is false) + { + return IsValidPropertyValue(property, null); + } + + return vvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); } /// diff --git a/src/Umbraco.Core/Services/PublishResult.cs b/src/Umbraco.Core/Services/PublishResult.cs index f689249afc..4e009cb49c 100644 --- a/src/Umbraco.Core/Services/PublishResult.cs +++ b/src/Umbraco.Core/Services/PublishResult.cs @@ -11,7 +11,7 @@ public class PublishResult : OperationResult /// /// Initializes a new instance of the class. /// - public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent? content) + public PublishResult(PublishResultType resultType, EventMessages? eventMessages, IContent content) : base(resultType, eventMessages, content) { } @@ -27,7 +27,7 @@ public class PublishResult : OperationResult /// /// Gets the document. /// - public IContent? Content => Entity; + public IContent Content => Entity ?? throw new InvalidOperationException("The content entity was null. Nullability must have been circumvented when constructing this instance. Please don't do that."); /// /// Gets or sets the invalid properties, if the status failed due to validation. diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index b8ebd5edd4..0eb0cdca9a 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -132,7 +132,12 @@ public enum PublishResultType : byte /// /// The document could not be published because it has been modified by another user. /// - FailedPublishConcurrencyViolation = FailedPublish | 11, + FailedPublishConcurrencyViolation = FailedPublish | 13, + + /// + /// The document could not be published because it has unsaved changes (is dirty). + /// + FailedPublishUnsavedChanges = FailedPublish | 14, #endregion diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 3776e4b703..14a5eb8d08 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1543,7 +1543,9 @@ public class ContentController : ContentControllerBase if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) { //its invariant, proceed normally - IEnumerable publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent!, force, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + _contentService.Save(contentItem.PersistedContent!); + IEnumerable publishStatus = _contentService.PublishBranch(contentItem.PersistedContent!, force, Array.Empty(), userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = null; //must be null! this implies invariant @@ -1578,7 +1580,9 @@ public class ContentController : ContentControllerBase if (canPublish) { //proceed to publish if all validation still succeeds - IEnumerable publishStatus = _contentService.SaveAndPublishBranch( + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + _contentService.Save(contentItem.PersistedContent!); + IEnumerable publishStatus = _contentService.PublishBranch( contentItem.PersistedContent!, force, culturesToPublish.WhereNotNull().ToArray(), _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); @@ -1592,7 +1596,7 @@ public class ContentController : ContentControllerBase OperationResult saveResult = _contentService.Save(contentItem.PersistedContent!, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); PublishResult[] publishStatus = { - new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent) + new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent!) }; wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; successfulCultures = Array.Empty(); @@ -1618,7 +1622,9 @@ public class ContentController : ContentControllerBase if (!contentItem.PersistedContent?.ContentType.VariesByCulture() ?? false) { //its invariant, proceed normally - PublishResult publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent!, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + _contentService.Save(contentItem.PersistedContent!); + PublishResult publishStatus = _contentService.Publish(contentItem.PersistedContent!, new [] {"*"}, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = null; //must be null! this implies invariant return publishStatus; @@ -1658,17 +1664,12 @@ public class ContentController : ContentControllerBase var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).WhereNotNull().ToArray(); canPublish = canPublish && culturesToPublish.Length > 0; - if (canPublish) - { - //try to publish all the values on the model - this will generally only fail if someone is tampering with the request - //since there's no reason variant rules would be violated in normal cases. - canPublish = PublishCulture(contentItem.PersistedContent!, variants, defaultCulture); - } - if (canPublish) { //proceed to publish if all validation still succeeds - PublishResult publishStatus = _contentService.SaveAndPublish( + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + _contentService.Save(contentItem.PersistedContent!); + PublishResult publishStatus = _contentService.Publish( contentItem.PersistedContent!, culturesToPublish, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); @@ -1683,7 +1684,7 @@ public class ContentController : ContentControllerBase OperationResult saveResult = _contentService.Save( contentItem.PersistedContent!, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1); - var publishStatus = new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent); + var publishStatus = new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent!); wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; successfulCultures = Array.Empty(); return publishStatus; @@ -1839,32 +1840,6 @@ public class ContentController : ContentControllerBase return canPublish; } - /// - /// Call PublishCulture on the content item for each culture to get a validation result for each culture - /// - /// - /// - /// - /// - /// - /// This would generally never fail unless someone is tampering with the request - /// - private bool PublishCulture(IContent persistentContent, IEnumerable cultureVariants, string? defaultCulture) - { - foreach (ContentVariantSave variant in cultureVariants.Where(x => x.Publish)) - { - // publishing any culture, implies the invariant culture - var valid = persistentContent.PublishCulture(_cultureImpactFactory.ImpactExplicit(variant.Culture, defaultCulture.InvariantEquals(variant.Culture))); - if (!valid) - { - AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles", "contentCultureValidationError"); - return false; - } - } - - return true; - } - private IEnumerable GetPublishedCulturesFromAncestors(IContent? content) { if (content?.ParentId is not -1 && content?.HasIdentity is false) @@ -1954,7 +1929,8 @@ public class ContentController : ContentControllerBase return HandleContentNotFound(id); } - PublishResult publishResult = _contentService.SaveAndPublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + PublishResult publishResult = _contentService.Publish(foundContent, foundContent.AvailableCultures.ToArray(), userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); if (publishResult.Success == false) { @@ -1997,7 +1973,8 @@ public class ContentController : ContentControllerBase foreach (var culture in model.Cultures) { - PublishResult publishResult = _contentService.SaveAndPublish(foundContent, culture, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); + // NOTE: we don't really care about the correctness of this anymore, as this controller is being replaced by the Management API ... let's just ensure that save and publish works for happy paths + PublishResult publishResult = _contentService.Publish(foundContent, new[] { culture }, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); results[culture] = publishResult; } diff --git a/tests/Umbraco.TestData/LoadTestController.cs b/tests/Umbraco.TestData/LoadTestController.cs index cb4a8d28bb..75599414ac 100644 --- a/tests/Umbraco.TestData/LoadTestController.cs +++ b/tests/Umbraco.TestData/LoadTestController.cs @@ -224,7 +224,8 @@ public class LoadTestController : Controller _contentTypeService.Save(containerType); var content = _contentService.Create("LoadTestContainer", -1, ContainerAlias); - _contentService.SaveAndPublish(content); + _contentService.Save(content); + _contentService.Publish(content, content.AvailableCultures.ToArray()); return ContentHtml("Installed."); } @@ -276,7 +277,8 @@ public class LoadTestController : Controller var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o; var content = _contentService.Create(name, s_containerId, ContentAlias); content.SetValue("origin", o); - _contentService.SaveAndPublish(content); + _contentService.Save(content); + _contentService.Publish(content, content.AvailableCultures.ToArray()); } if (restart) diff --git a/tests/Umbraco.TestData/UmbracoTestDataController.cs b/tests/Umbraco.TestData/UmbracoTestDataController.cs index 5a7e59b32c..5743a746a8 100644 --- a/tests/Umbraco.TestData/UmbracoTestDataController.cs +++ b/tests/Umbraco.TestData/UmbracoTestDataController.cs @@ -86,7 +86,7 @@ public class UmbracoTestDataController : SurfaceController var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); - Services.ContentService.SaveAndPublishBranch(root, true); + Services.ContentService.PublishBranch(root, true, new[] { "*" }); scope.Complete(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index c03cd5b6f0..eb9f032bfa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -78,7 +78,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent protected override void CustomTestSetup(IUmbracoBuilder builder) => builder .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler(); [Test] public void Create_Blueprint() @@ -228,7 +229,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent } else { - var r = ContentService.SaveAndPublish(c); + ContentService.Save(c); + var r = ContentService.Publish(c, c.AvailableCultures.ToArray()); var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, now.AddSeconds(5)); // expire in 5 seconds @@ -261,7 +263,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent } else { - var r = ContentService.SaveAndPublish(c); + ContentService.Save(c); + var r = ContentService.Publish(c, c.AvailableCultures.ToArray()); var contentSchedule = ContentScheduleCollection.CreateWithEntry(alternatingCulture, null, @@ -331,7 +334,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent contentSchedule = ContentService.GetContentScheduleByContentId(content.Id); sched = contentSchedule.FullSchedule; Assert.AreEqual(0, sched.Count); - Assert.IsTrue(ContentService.SaveAndPublish(content).Success); + Assert.IsTrue(ContentService.Publish(content, content.AvailableCultures.ToArray()).Success); } [Test] @@ -344,7 +347,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent for (var i = 0; i < 20; i++) { content.SetValue("bodyText", "hello world " + Guid.NewGuid()); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); } // Assert @@ -578,7 +582,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { var parent = ContentService.GetById(Textpage.Id); Assert.IsFalse(parent.Published); - ContentService.SaveAndPublish(parent); // publishing parent, so Text Page 2 can be updated. + ContentService.Save(parent); // publishing parent, so Text Page 2 can be updated. + ContentService.Publish(parent, parent.AvailableCultures.ToArray()); var content = ContentService.GetById(Subpage.Id); Assert.IsFalse(content.Published); @@ -590,14 +595,16 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.Name = "Text Page 2 Updated"; content.SetValue("author", "Jane Doe"); - ContentService.SaveAndPublish(content); // publishes the current version, creates a version + ContentService.Save(content); // publishes the current version, creates a version + ContentService.Publish(content, content.AvailableCultures.ToArray()); var version2 = content.VersionId; Console.WriteLine($"2 e={content.VersionId} p={content.PublishedVersionId}"); content.Name = "Text Page 2 ReUpdated"; content.SetValue("author", "Bob Hope"); - ContentService.SaveAndPublish(content); // publishes again, creates a version + ContentService.Save(content); // publishes again, creates a version + ContentService.Publish(content, content.AvailableCultures.ToArray()); var version3 = content.VersionId; Console.WriteLine($"3 e={content.VersionId} p={content.PublishedVersionId}"); @@ -664,11 +671,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var root = ContentService.GetById(Textpage.Id); - ContentService.SaveAndPublish(root); + ContentService.Publish(root!, root!.AvailableCultures.ToArray()); var content = ContentService.GetById(Subpage.Id); var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddSeconds(1)); - ContentService.PersistContentSchedule(content, contentSchedule); - ContentService.SaveAndPublish(content); + ContentService.PersistContentSchedule(content!, contentSchedule); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // Act Thread.Sleep(new TimeSpan(0, 0, 0, 2)); @@ -712,7 +719,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Textpage.Id); - var published = ContentService.SaveAndPublish(content, userId: -1); + Assert.IsNotNull(content); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: -1); // Act var unpublished = ContentService.Unpublish(content, userId: -1); @@ -730,17 +738,14 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault)); - content.PublishCulture(CultureImpact.Explicit(langUk.IsoCode, langUk.IsDefault)); - Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); - Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); // re-get content = ContentService.GetById(content.Id); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); @@ -764,7 +769,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.AreEqual(PublishedState.Published, content.PublishedState); // re-get @@ -786,7 +792,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content = ContentService.GetById(content.Id); - published = ContentService.SaveAndPublish(content, langUk.IsoCode); + published = ContentService.Publish(content, new[] { langUk.IsoCode }); Assert.AreEqual(PublishedState.Published, content.PublishedState); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); @@ -803,9 +809,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); Assert.AreEqual(PublishedState.Published, content.PublishedState); @@ -865,9 +873,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.SetCultureName("content-fr", langFr.IsoCode); content.SetCultureName("content-en", langUk.IsoCode); - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); Assert.AreEqual(PublishedState.Published, content.PublishedState); @@ -888,9 +898,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); Assert.AreEqual(PublishedState.Published, content.PublishedState); @@ -924,9 +936,11 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var published = ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); Assert.AreEqual(PublishedState.Published, content.PublishedState); @@ -936,8 +950,10 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // Change some data since SaveAndPublish should always Save content.SetCultureName("content-en-updated", langUk.IsoCode); - var saved = ContentService.SaveAndPublish(content, new string[] { }); // save without cultures - Assert.AreEqual(PublishResultType.FailedPublishNothingToPublish, saved.Result); + saved = ContentService.Save(content); + published = ContentService.Publish(content, new string[] { }); // publish without cultures + Assert.IsTrue(saved.Success); + Assert.AreEqual(PublishResultType.FailedPublishNothingToPublish, published.Result); // re-get content = ContentService.GetById(content.Id); @@ -977,7 +993,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.SetCultureName("content-en", langGb.IsoCode); content.SetCultureName("content-fr", langFr.IsoCode); - Assert.IsTrue(ContentService.SaveAndPublish(content, new[] { langGb.IsoCode, langFr.IsoCode }).Success); + Assert.IsTrue(ContentService.Save(content).Success); + Assert.IsTrue(ContentService.Publish(content, new[] { langGb.IsoCode, langFr.IsoCode }).Success); // re-get content = ContentService.GetById(content.Id); @@ -1007,7 +1024,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent IContent content = new Content("content", Constants.System.Root, contentType); content.SetCultureName("content-fr", langFr.IsoCode); - var published = ContentService.SaveAndPublish(content, langFr.IsoCode); + ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langFr.IsoCode }); // audit log will only show that french was published var lastLog = AuditService.GetLogs(content.Id).Last(); @@ -1016,7 +1034,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // re-get content = ContentService.GetById(content.Id); content.SetCultureName("content-en", langUk.IsoCode); - published = ContentService.SaveAndPublish(content, langUk.IsoCode); + ContentService.Save(content); + published = ContentService.Publish(content, new[] { langUk.IsoCode }); // audit log will only show that english was published lastLog = AuditService.GetLogs(content.Id).Last(); @@ -1046,7 +1065,9 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent IContent content = new Content("content", Constants.System.Root, contentType); content.SetCultureName("content-fr", langFr.IsoCode); content.SetCultureName("content-gb", langGb.IsoCode); - var published = ContentService.SaveAndPublish(content, new[] { langGb.IsoCode, langFr.IsoCode }); + var saved = ContentService.Save(content); + var published = ContentService.Publish(content, new[] { langGb.IsoCode, langFr.IsoCode }); + Assert.IsTrue(saved.Success); Assert.IsTrue(published.Success); // re-get @@ -1073,9 +1094,10 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Textpage.Id); + Assert.IsNotNull(content); // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.True); @@ -1087,9 +1109,10 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Textpage.Id); + Assert.IsNotNull(content); // Act - var published = ContentService.SaveAndPublish(content, userId: -1); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: -1); // Assert Assert.That(published.Success, Is.True); @@ -1102,7 +1125,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // Arrange var parent = ContentService.Create("parent", Constants.System.Root, "umbTextpage"); - ContentService.SaveAndPublish(parent); + ContentService.Save(parent); + ContentService.Publish(parent, parent.AvailableCultures.ToArray()); var content = ContentService.Create("child", parent, "umbTextpage"); ContentService.Save(content); @@ -1114,31 +1138,39 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent [Test] public void Can_Publish_Content_WithEvents() { + var savingWasCalled = false; var publishingWasCalled = false; + ContentNotificationHandler.SavingContent = notification => + { + Assert.AreEqual(1, notification.SavedEntities.Count()); + var entity = notification.SavedEntities.First(); + Assert.AreEqual("foo", entity.Name); + + var e = ContentService.GetById(entity.Id); + Assert.AreEqual("Home", e.Name); + + savingWasCalled = true; + }; + ContentNotificationHandler.PublishingContent = notification => { Assert.AreEqual(1, notification.PublishedEntities.Count()); var entity = notification.PublishedEntities.First(); Assert.AreEqual("foo", entity.Name); - var e = ContentService.GetById(entity.Id); - Assert.AreEqual("Home", e.Name); - publishingWasCalled = true; }; - // tests that during 'publishing' event, what we get from the repo is the 'old' content, - // because 'publishing' fires before the 'saved' event ie before the content is actually - // saved try { var content = ContentService.GetById(Textpage.Id); Assert.AreEqual("Home", content.Name); content.Name = "foo"; + ContentService.Save(content); var published = - ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); Assert.That(published.Success, Is.True); Assert.That(content.Published, Is.True); @@ -1146,29 +1178,49 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var e = ContentService.GetById(content.Id); Assert.AreEqual("foo", e.Name); + Assert.IsTrue(savingWasCalled); Assert.IsTrue(publishingWasCalled); } finally { + ContentNotificationHandler.SavingContent = null; ContentNotificationHandler.PublishingContent = null; } } [Test] - public void Can_Not_Publish_Invalid_Cultures() + public void Can_Not_Publish_Invalid_Cultures_For_Variant_Content() { - var content = new ContentBuilder() - .AddContentType() - .WithContentVariation(ContentVariation.Culture) - .Done() - .Build(); + var contentType = ContentTypeBuilder.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ContentTypeService.Save(contentType); - Assert.Throws(() => ContentService.SaveAndPublish(content, new[] { "*" })); - Assert.Throws( - () => ContentService.SaveAndPublish(content, new string[] { null })); - Assert.Throws(() => ContentService.SaveAndPublish(content, new[] { "*", null })); - Assert.Throws(() => - ContentService.SaveAndPublish(content, new[] { "en-US", "*", "es-ES" })); + var content = ContentBuilder.CreateBasicContent(contentType); + content.SetCultureName("Name for en-US", "en-US"); + ContentService.Save(content); + + Assert.Throws(() => ContentService.Publish(content, null!)); + Assert.Throws(() => ContentService.Publish(content, new string[] { null })); + Assert.Throws(() => ContentService.Publish(content, new [] { string.Empty })); + Assert.Throws(() => ContentService.Publish(content, new[] { "*", null })); + Assert.Throws(() => ContentService.Publish(content, new[] { "en-US", "*" })); + } + + [Test] + public void Can_Not_Publish_Invalid_Cultures_For_Invariant_Content() + { + var contentType = ContentTypeBuilder.CreateBasicContentType(); + ContentTypeService.Save(contentType); + + var content = ContentBuilder.CreateBasicContent(contentType); + content.Name = "Content name"; + ContentService.Save(content); + + Assert.Throws(() => ContentService.Publish(content, null!)); + Assert.Throws(() => ContentService.Publish(content, new string[] { null })); + Assert.Throws(() => ContentService.Publish(content, new[] { "*", null })); + Assert.Throws(() => ContentService.Publish(content, new[] { "en-US" })); + Assert.Throws(() => ContentService.Publish(content, new[] { "en-US", "*" })); } [Test] @@ -1185,7 +1237,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var parent = ContentService.GetById(parentId); - var parentPublished = ContentService.SaveAndPublish(parent); + ContentService.Save(parent); + var parentPublished = ContentService.Publish(parent, parent.AvailableCultures.ToArray()); // parent can publish values // and therefore can be published @@ -1206,7 +1259,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // and therefore cannot be published, // because it did not have a published version at all - var contentPublished = ContentService.SaveAndPublish(content); + ContentService.Save(content); + var contentPublished = ContentService.Publish(content, content.AvailableCultures.ToArray()); Assert.IsFalse(contentPublished.Success); Assert.AreEqual(PublishResultType.FailedPublishContentInvalid, contentPublished.Result); Assert.IsFalse(content.Published); @@ -1305,7 +1359,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // publish parent & its branch // only those that are not already published // only invariant/neutral values - var parentPublished = ContentService.SaveAndPublishBranch(parent, true); + var parentPublished = ContentService.PublishBranch(parent, true, parent.AvailableCultures.ToArray()); foreach (var result in parentPublished) { @@ -1334,13 +1388,15 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(content, contentSchedule: contentSchedule); var parent = ContentService.GetById(Textpage.Id); + Assert.IsNotNull(parent); var parentPublished = - ContentService.SaveAndPublish(parent, + ContentService.Publish(parent, + parent.AvailableCultures.ToArray(), userId: Constants.Security .SuperUserId); // Publish root Home node to enable publishing of 'Subpage.Id' // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1361,7 +1417,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.Now.AddMinutes(-5)); ContentService.Save(content, contentSchedule: contentSchedule); - var published = ContentService.SaveAndPublish(content, "en-US"); + var published = ContentService.Publish(content, new[] { "en-US" }); Assert.IsFalse(published.Success); Assert.AreEqual(PublishResultType.FailedPublishCultureHasExpired, published.Result); @@ -1377,13 +1433,15 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); var parent = ContentService.GetById(Textpage.Id); + Assert.IsNotNull(parent); var parentPublished = - ContentService.SaveAndPublish(parent, + ContentService.Publish(parent, + parent.AvailableCultures.ToArray(), userId: Constants.Security .SuperUserId); // Publish root Home node to enable publishing of 'Subpage.Id' // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(parentPublished.Success, Is.True); @@ -1422,7 +1480,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); content.Properties[0].SetValue("Foo", string.Empty); contentService.Save(content); @@ -1430,7 +1489,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); // Act - var result = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var result = contentService.Publish(content, Array.Empty(), userId: Constants.Security.SuperUserId); // Assert Assert.Multiple(() => @@ -1475,14 +1534,15 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); contentService.PersistContentSchedule(content, ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); contentService.Save(content); // Act - var result = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var result = contentService.Publish(content, Array.Empty(), userId: Constants.Security.SuperUserId); // Assert Assert.Multiple(() => @@ -1509,7 +1569,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.Now.AddHours(2), null); ContentService.Save(content, contentSchedule: contentSchedule); - var published = ContentService.SaveAndPublish(content, "en-US"); + var published = ContentService.Publish(content, new[] { "en-US" }); Assert.IsFalse(published.Success); Assert.AreEqual(PublishResultType.FailedPublishCultureAwaitingRelease, published.Result); @@ -1524,7 +1584,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentService.Save(content); // Act - var published = ContentService.SaveAndPublishBranch(content, true); + var published = ContentService.PublishBranch(content, true, content.AvailableCultures.ToArray()); // Assert Assert.That(published.All(x => x.Success), Is.False); @@ -1536,9 +1596,10 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Trashed.Id); + Assert.IsNotNull(content); // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(published.Success, Is.False); @@ -1554,12 +1615,14 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.SetValue("author", "Barack Obama"); // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var saved = ContentService.Save(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); Assert.That(content.Published, Is.True); Assert.IsTrue(published.Success); + Assert.IsTrue(saved.Success); } /// @@ -1577,15 +1640,17 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.SetValue("author", "Barack Obama"); // Act - var published = ContentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + var saved = ContentService.Save(content, userId: Constants.Security.SuperUserId); + var published = ContentService.Publish(content, content.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); var childContent = ContentService.Create("Child", content.Id, "umbTextpage"); // Reset all identity properties childContent.Id = 0; childContent.Path = null; ((Content)childContent).ResetIdentity(); + var childSaved = ContentService.Save(childContent, userId: Constants.Security.SuperUserId); var childPublished = - ContentService.SaveAndPublish(childContent, userId: Constants.Security.SuperUserId); + ContentService.Publish(childContent, childContent.AvailableCultures.ToArray(), userId: Constants.Security.SuperUserId); // Assert Assert.That(content.HasIdentity, Is.True); @@ -1594,6 +1659,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.That(childContent.Published, Is.True); Assert.That(published.Success, Is.True); Assert.That(childPublished.Success, Is.True); + Assert.That(saved.Success, Is.True); + Assert.That(childSaved.Success, Is.True); } [Test] @@ -1601,12 +1668,13 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent public void Can_Get_Published_Descendant_Versions() { // Arrange - var root = ContentService.GetById(Textpage.Id); - var rootPublished = ContentService.SaveAndPublish(root); + var root = ContentService.GetById(Textpage.Id)!; + var rootPublished = ContentService.Publish(root, root.AvailableCultures.ToArray()); var content = ContentService.GetById(Subpage.Id); content.Properties["title"].SetValue(content.Properties["title"].GetValue() + " Published"); - var contentPublished = ContentService.SaveAndPublish(content); + ContentService.Save(content); + var contentPublished = ContentService.Publish(content, content.AvailableCultures.ToArray()); var publishedVersion = content.VersionId; content.Properties["title"].SetValue(content.Properties["title"].GetValue() + " Saved"); @@ -1667,14 +1735,16 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent IContent content = ContentBuilder.CreateSimpleContent(contentType, "hello"); content.SetValue("title", "title of mine"); content.SetValue("bodyText", "hello world"); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // re-get content = ContentService.GetById(content.Id); content.SetValue("title", "another title of mine"); // Change a value content.SetValue("bodyText", null); // Clear a value content.SetValue("author", "new author"); // Add a value - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // re-get content = ContentService.GetById(content.Id); @@ -1979,12 +2049,12 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content1.PropertyValues(obj); content1.ResetDirtyProperties(false); ContentService.Save(content1); - Assert.IsTrue(ContentService.SaveAndPublish(content1, userId: -1).Success); + Assert.IsTrue(ContentService.Publish(content1, content1.AvailableCultures.ToArray(), userId: -1).Success); var content2 = ContentBuilder.CreateBasicContent(contentType); content2.PropertyValues(obj); content2.ResetDirtyProperties(false); ContentService.Save(content2); - Assert.IsTrue(ContentService.SaveAndPublish(content2, userId: -1).Success); + Assert.IsTrue(ContentService.Publish(content2, content2.AvailableCultures.ToArray(), userId: -1).Success); var editorGroup = UserService.GetUserGroupByAlias(Constants.Security.EditorGroupAlias); editorGroup.StartContentId = content1.Id; @@ -2188,7 +2258,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.AreEqual(0, contentTags.Length); // publish - ContentService.SaveAndPublish(content); + ContentService.Publish(content, new []{ "*" }); // now tags have been set (published) Assert.AreEqual("[\"hello\",\"world\"]", content.GetValue(propAlias)); @@ -2204,7 +2274,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.AreEqual(0, copiedTags.Length); // publish - ContentService.SaveAndPublish(copy); + ContentService.Publish(copy, new []{ "*" }); // now tags have been set (published) copiedTags = TagService.GetTagsForEntity(copy.Id).ToArray(); @@ -2220,7 +2290,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // Arrange var parent = ContentService.GetById(Textpage.Id); Assert.IsFalse(parent.Published); - ContentService.SaveAndPublish(parent); // publishing parent, so Text Page 2 can be updated. + ContentService.Save(parent); + ContentService.Publish(parent, parent.AvailableCultures.ToArray()); // publishing parent, so Text Page 2 can be updated. var content = ContentService.GetById(Subpage.Id); Assert.IsFalse(content.Published); @@ -2236,7 +2307,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // non published = edited Assert.IsTrue(content.Edited); - ContentService.SaveAndPublish(content); // new version + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // new version var version2 = content.VersionId; Assert.AreNotEqual(version1, version2); @@ -2261,7 +2333,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.Name = "Text Page 2 ReReUpdated"; - ContentService.SaveAndPublish(content); // new version + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // new version var version3 = content.VersionId; Assert.AreNotEqual(version2, version3); @@ -2318,7 +2391,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content = ContentService.GetById(content.Id); Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); Assert.AreEqual("Jane Doe", content.GetValue("author")); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); Assert.IsFalse(content.Edited); content.Name = "Xxx"; content.SetValue("author", "Bob Doe"); @@ -2375,7 +2449,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent page.SetValue(p1.Alias, "v1fr", langFr.IsoCode); page.SetValue(p1.Alias, "v1da", langDa.IsoCode); Thread.Sleep(1); - ContentService.SaveAndPublish(page); + ContentService.Save(page); + ContentService.Publish(page, page.AvailableCultures.ToArray()); var versionId1 = page.VersionId; Thread.Sleep(10); @@ -2383,7 +2458,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent page.SetCultureName("fr2", langFr.IsoCode); page.SetValue(p1.Alias, "v2fr", langFr.IsoCode); Thread.Sleep(1); - ContentService.SaveAndPublish(page, langFr.IsoCode); + ContentService.Save(page); + ContentService.Publish(page, new[] { langFr.IsoCode }); var versionId2 = page.VersionId; Thread.Sleep(10); @@ -2391,7 +2467,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent page.SetCultureName("da2", langDa.IsoCode); page.SetValue(p1.Alias, "v2da", langDa.IsoCode); Thread.Sleep(1); - ContentService.SaveAndPublish(page, langDa.IsoCode); + ContentService.Save(page); + ContentService.Publish(page, new[] { langDa.IsoCode }); var versionId3 = page.VersionId; Thread.Sleep(10); @@ -2401,7 +2478,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent page.SetValue(p1.Alias, "v3fr", langFr.IsoCode); page.SetValue(p1.Alias, "v3da", langDa.IsoCode); Thread.Sleep(1); - ContentService.SaveAndPublish(page); + ContentService.Save(page); + ContentService.Publish(page, page.AvailableCultures.ToArray()); var versionId4 = page.VersionId; // now get all versions @@ -2778,7 +2856,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // becomes Published, !Edited // creates a new version // can get published property values - ContentService.SaveAndPublish(content); + ContentService.Publish(content, new []{ "*" }); Assert.IsTrue(content.Published); Assert.IsFalse(content.Edited); @@ -2830,7 +2908,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // and therefore we cannot "just" republish the content - we need to publish some values // so... that's not really an option // - // ContentService.SaveAndPublish(content); + // ContentService.Publish(content, new []{ "*" }); // TODO: what shall we do of all this? /* @@ -3161,7 +3239,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent AssertPerCulture(content2, (x, c) => x.IsCultureEdited(c), (langFr, true), (langUk, true), (langDe, false)); // Act - ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + ContentService.Publish(content, new[] { langFr.IsoCode, langUk.IsoCode }); // both FR and UK have been published, // and content has been published, @@ -3211,7 +3289,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // note that content and content2 culture published dates might be slightly different due to roundtrip to database // Act - ContentService.SaveAndPublish(content); + ContentService.Publish(content, new []{ "*" }); // now it has publish name for invariant neutral content2 = ContentService.GetById(content.Id); @@ -3413,7 +3491,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent (langUk, false)); // FR, DE would throw // Act - ContentService.SaveAndPublish(content, langUk.IsoCode); + ContentService.Publish(content, new[] { langUk.IsoCode }); content2 = ContentService.GetById(content.Id); @@ -3453,6 +3531,78 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent Assert.IsTrue(content2.IsCultureEdited(langUk.IsoCode)); } + [Test] + public void Cannot_Publish_Newly_Created_Unsaved_Content() + { + var content = ContentService.Create("Test", Constants.System.Root, "umbTextpage"); + var publishResult = ContentService.Publish(content, new[] { "*" }); + Assert.AreEqual(PublishResultType.FailedPublishUnsavedChanges, publishResult.Result); + } + + [Test] + public void Cannot_Publish_Unsaved_Content() + { + var content = ContentService.Create("Test", Constants.System.Root, "umbTextpage"); + ContentService.Save(content); + content.Name = "Test2"; + + var publishResult = ContentService.Publish(content, new[] { "*" }); + Assert.AreEqual(PublishResultType.FailedPublishUnsavedChanges, publishResult.Result); + } + + [Test] + public async Task Cannot_Publish_Invalid_Variant_Content() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(content); + + // reset any state and attempt a publish + content = ContentService.GetById(content.Key)!; + var result = ContentService.Publish(content, new[] { "*" }); + + Assert.IsFalse(result.Success); + Assert.AreEqual(PublishResultType.FailedPublishContentInvalid, result.Result); + + // verify saved state + content = ContentService.GetById(content.Key)!; + Assert.IsEmpty(content.PublishedCultures); + } + + [Test] + public async Task Can_Publish_Culture_With_Other_Culture_Invalid() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(content); + + // reset any state and attempt a publish + content = ContentService.GetById(content.Key)!; + var result = ContentService.Publish(content, new[] { langEn.IsoCode }); + + Assert.IsTrue(result.Success); + Assert.AreEqual(PublishResultType.SuccessPublishCulture, result.Result); + + // verify saved state + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(1, content.PublishedCultures.Count()); + Assert.AreEqual(langEn.IsoCode.ToLowerInvariant(), content.PublishedCultures.First()); + } + private void AssertPerCulture(IContent item, Func getter, params (ILanguage Language, bool Result)[] testCases) { @@ -3533,7 +3683,8 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent public class ContentNotificationHandler : INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler { public static Action PublishingContent { get; set; } @@ -3541,9 +3692,47 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent public static Action CopiedContent { get; set; } + public static Action SavingContent { get; set; } + public void Handle(ContentCopiedNotification notification) => CopiedContent?.Invoke(notification); public void Handle(ContentCopyingNotification notification) => CopyingContent?.Invoke(notification); public void Handle(ContentPublishingNotification notification) => PublishingContent?.Invoke(notification); + + public void Handle(ContentSavingNotification notification) => SavingContent?.Invoke(notification); + } + + private async Task<(ILanguage LangEn, ILanguage LangDa, IContentType contentType)> SetupVariantTest() + { + var langEn = (await LanguageService.GetAsync("en-US"))!; + var langDa = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey); + + var template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + var contentType = new ContentTypeBuilder() + .WithAlias("variantContent") + .WithName("Variant Content") + .WithContentVariation(ContentVariation.Culture) + .AddPropertyGroup() + .WithAlias("content") + .WithName("Content") + .WithSupportsPublishing(true) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithVariations(ContentVariation.Culture) + .WithMandatory(true) + .Done() + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + ContentTypeService.Save(contentType); + + return (langEn, langDa, contentType); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Variants/ContentVariantAllowedActionTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Variants/ContentVariantAllowedActionTests.cs index c68742580f..116a770605 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Variants/ContentVariantAllowedActionTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Variants/ContentVariantAllowedActionTests.cs @@ -71,7 +71,8 @@ public class ContentVariantAllowedActionTests : UmbracoTestServerTestBase .Build(); var contentService = GetRequiredService(); - contentService.SaveAndPublish(rootNode); + contentService.Save(rootNode); + contentService.Publish(rootNode, new[] { "*" }); ContentItemDisplay? display = UmbracoMapper.Map(rootNode, context => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs index 8636869a56..fa4d95fd3f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentVersionRepositoryTest.cs @@ -35,11 +35,12 @@ public class DocumentVersionRepositoryTest : UmbracoIntegrationTest ContentTypeService.Save(contentType); var content = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); // At this point content has 2 versions, a draft version and a published version. - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); // At this point content has 3 versions, a historic version, a draft version and a published version. using (ScopeProvider.CreateScope()) @@ -67,12 +68,13 @@ public class DocumentVersionRepositoryTest : UmbracoIntegrationTest ContentTypeService.Save(contentType); var content = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); // At this point content has 2 versions, a draft version and a published version. - ContentService.SaveAndPublish(content); - ContentService.SaveAndPublish(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); + ContentService.Publish(content, Array.Empty()); + ContentService.Publish(content, Array.Empty()); // At this point content has 5 versions, 3 historic versions, a draft version and a published version. var allVersions = ContentService.GetVersions(content.Id); @@ -109,11 +111,12 @@ public class DocumentVersionRepositoryTest : UmbracoIntegrationTest ContentTypeService.Save(contentType); var content = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(content); - ContentService.SaveAndPublish(content); - ContentService.SaveAndPublish(content); - ContentService.SaveAndPublish(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); + ContentService.Publish(content, Array.Empty()); + ContentService.Publish(content, Array.Empty()); + ContentService.Publish(content, Array.Empty()); using (var scope = ScopeProvider.CreateScope()) { var query = ScopeAccessor.AmbientScope.SqlContext.Sql(); @@ -146,9 +149,10 @@ public class DocumentVersionRepositoryTest : UmbracoIntegrationTest ContentTypeService.Save(contentType); var content = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(content); - ContentService.SaveAndPublish(content); // Draft + Published - ContentService.SaveAndPublish(content); // New Draft + ContentService.Publish(content, Array.Empty()); // Draft + Published + ContentService.Publish(content, Array.Empty()); // New Draft using (ScopeProvider.CreateScope()) { @@ -187,8 +191,9 @@ public class DocumentVersionRepositoryTest : UmbracoIntegrationTest var content = ContentBuilder.CreateSimpleContent(contentType, "foo", culture: "en-US"); content.SetCultureName("foo", "en-US"); - ContentService.SaveAndPublish(content, "en-US"); // Draft + Published - ContentService.SaveAndPublish(content, "en-US"); // New Draft + ContentService.Save(content); + ContentService.Publish(content, new[] { "en-US" }); // Draft + Published + ContentService.Publish(content, new[] { "en-US" }); // New Draft using (ScopeProvider.CreateScope()) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs index 777b4cafb8..e727060d98 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopedNuCacheTests.cs @@ -71,7 +71,8 @@ public class ScopedNuCacheTests : UmbracoIntegrationTest using (var scope = ScopeProvider.CreateScope()) { - ContentService.SaveAndPublish(item); + ContentService.Save(item); + ContentService.Publish(item, item.AvailableCultures.ToArray()); scope.Complete(); } @@ -96,7 +97,8 @@ public class ScopedNuCacheTests : UmbracoIntegrationTest using (var scope = ScopeProvider.CreateScope()) { item.Name = "changed"; - ContentService.SaveAndPublish(item); + ContentService.Save(item); + ContentService.Publish(item, item.AvailableCultures.ToArray()); if (complete) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs index fda265c705..ad77f68b10 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentEventsTests.cs @@ -211,15 +211,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services private IContent CreateBranch() { Content content1 = ContentBuilder.CreateSimpleContent(_contentType, "Content1"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); // 2 (published) // .1 (published) // .2 (not published) Content content2 = ContentBuilder.CreateSimpleContent(_contentType, "Content2", content1); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); Content content21 = ContentBuilder.CreateSimpleContent(_contentType, "Content21", content2); - ContentService.SaveAndPublish(content21); + ContentService.Save(content21); + ContentService.Publish(content21, content21.AvailableCultures.ToArray()); Content content22 = ContentBuilder.CreateSimpleContent(_contentType, "Content22", content2); ContentService.Save(content22); @@ -237,11 +240,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // .1 (published) // .2 (not published) Content content4 = ContentBuilder.CreateSimpleContent(_contentType, "Content4", content1); - ContentService.SaveAndPublish(content4); + ContentService.Save(content4); + ContentService.Publish(content4, content4.AvailableCultures.ToArray()); content4.Name = "Content4X"; ContentService.Save(content4); Content content41 = ContentBuilder.CreateSimpleContent(_contentType, "Content41", content4); - ContentService.SaveAndPublish(content41); + ContentService.Save(content41); + ContentService.Publish(content41, content41.AvailableCultures.ToArray()); Content content42 = ContentBuilder.CreateSimpleContent(_contentType, "Content42", content4); ContentService.Save(content42); @@ -249,9 +254,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // .1 (published) // .2 (not published) Content content5 = ContentBuilder.CreateSimpleContent(_contentType, "Content5", content1); - ContentService.SaveAndPublish(content5); + ContentService.Save(content5); + ContentService.Publish(content5, content5.AvailableCultures.ToArray()); Content content51 = ContentBuilder.CreateSimpleContent(_contentType, "Content51", content5); - ContentService.SaveAndPublish(content51); + ContentService.Save(content51); + ContentService.Publish(content51, content51.AvailableCultures.ToArray()); Content content52 = ContentBuilder.CreateSimpleContent(_contentType, "Content52", content5); ContentService.Save(content52); ContentService.Unpublish(content5); @@ -409,7 +416,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh newest IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); content.Name = "changed"; @@ -445,7 +452,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh newest IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); content.SortOrder = 666; @@ -481,7 +488,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh newest IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); content.Properties.First().SetValue("changed"); @@ -520,15 +527,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ResetEvents(); content.Name = "changed"; - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); - Assert.AreEqual(2, _msgCount); - Assert.AreEqual(2, _events.Count); + Assert.AreEqual(4, _msgCount); + Assert.AreEqual(4, _events.Count); int i = 0; int m = 0; - Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content.Id}.u+p", _events[i++].ToString()); - m++; - Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshBranch/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content.Id}.u=u", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content.Id}.u+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentCacheRefresher/RefreshBranch/{content.Id}", _events[i++].ToString()); } [Test] @@ -540,19 +549,21 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh published, newest IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); content.Name = "changed"; - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); - Assert.AreEqual(2, _msgCount); - Assert.AreEqual(2, _events.Count); + Assert.AreEqual(4, _msgCount); + Assert.AreEqual(4, _events.Count); int i = 0; int m = 0; - Assert.AreEqual($"{m:000}: ContentRepository/Refresh/{content.Id}.p+p", _events[i++].ToString()); - m++; - Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content.Id}.p=p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Refresh/{content.Id}.p+p", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentCacheRefresher/RefreshNode/{content.Id}", _events[i++].ToString()); } [Test] @@ -568,9 +579,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ResetEvents(); content.Name = "changed"; - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ResetEvents(); + ContentService.Publish(content, Array.Empty()); Assert.AreEqual(2, _msgCount); Assert.AreEqual(2, _events.Count); @@ -590,7 +602,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh newest, remove published IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); ContentService.Unpublish(content); @@ -613,7 +625,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // - content cache :: refresh newest, remove published IContent content = ContentService.GetRootContent().FirstOrDefault(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); content.Name = "changed"; ContentService.Save(content); @@ -673,7 +685,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ContentService.Unpublish(content1); ResetEvents(); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); Assert.AreEqual(2, _msgCount); Assert.AreEqual(2, _events.Count); @@ -706,7 +718,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // branch is: ResetEvents(); - ContentService.SaveAndPublishBranch(content1, force: false); // force = false, don't publish unpublished items + ContentService.PublishBranch(content1, force: false, cultures: content1.AvailableCultures.ToArray()); // force = false, don't publish unpublished items foreach (EventInstance e in _events) { @@ -743,7 +755,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ContentService.Unpublish(content1); ResetEvents(); - ContentService.SaveAndPublishBranch(content1, force: true); // force = true, also publish unpublished items + ContentService.PublishBranch(content1, force: true, cultures: content1.AvailableCultures.ToArray()); // force = true, also publish unpublished items foreach (EventInstance e in _events) { @@ -909,7 +921,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); ContentService.MoveToRecycleBin(content); @@ -932,7 +944,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ContentService.MoveToRecycleBin(content); ResetEvents(); @@ -958,7 +970,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); content.Properties.First().SetValue("changed"); ContentService.Save(content); @@ -1137,7 +1149,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); ContentService.Delete(content); @@ -1157,7 +1169,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); content.Properties.First().SetValue("changed"); ContentService.Save(content); @@ -1179,10 +1191,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Unpublish(content1); ResetEvents(); @@ -1267,7 +1279,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(); Assert.IsNotNull(content2); @@ -1289,7 +1301,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); content1.Properties.First().SetValue("changed"); ContentService.Save(content1); IContent content2 = CreateContent(); @@ -1315,7 +1327,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.IsNotNull(content1); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content1, content2.Id); @@ -1337,10 +1349,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.IsNotNull(content1); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); IContent content3 = CreateContent(); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ContentService.Unpublish(content2); ResetEvents(); @@ -1360,10 +1372,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content1, content2.Id); @@ -1382,13 +1394,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); IContent content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ContentService.Unpublish(content2); ResetEvents(); @@ -1409,12 +1421,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); content1.Properties.First().SetValue("changed"); ContentService.Save(content1); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content1, content2.Id); @@ -1433,15 +1445,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); content1.Properties.First().SetValue("changed"); ContentService.Save(content1); IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); IContent content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ContentService.Unpublish(content2); ResetEvents(); @@ -1462,14 +1474,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Unpublish(content1); IContent content3 = CreateContent(); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content2, content3.Id); @@ -1489,17 +1501,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Unpublish(content1); IContent content3 = CreateContent(); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); IContent content4 = CreateContent(content3.Id); Assert.IsNotNull(content4); - ContentService.SaveAndPublish(content4); + ContentService.Publish(content4, content4.AvailableCultures.ToArray()); ContentService.Unpublish(content3); ResetEvents(); @@ -1519,16 +1531,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); content2.Properties.First().SetValue("changed"); ContentService.Save(content2); ContentService.Unpublish(content1); IContent content3 = CreateContent(); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content2, content3.Id); @@ -1548,19 +1560,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); content2.Properties.First().SetValue("changed"); ContentService.Save(content2); ContentService.Unpublish(content1); IContent content3 = CreateContent(); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); IContent content4 = CreateContent(content3.Id); Assert.IsNotNull(content4); - ContentService.SaveAndPublish(content4); + ContentService.Publish(content4, content4.AvailableCultures.ToArray()); ContentService.Unpublish(content3); ResetEvents(); @@ -1581,10 +1593,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Unpublish(content1); IContent content3 = CreateContent(); Assert.IsNotNull(content3); @@ -1606,10 +1618,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content1 = CreateContent(); Assert.IsNotNull(content1); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = CreateContent(content1.Id); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); content2.Properties.First().SetValue("changed"); ContentService.Save(content2); ContentService.Unpublish(content1); @@ -1678,7 +1690,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ResetEvents(); ContentService.Move(content1, content2.Id); @@ -1720,10 +1732,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); IContent content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ContentService.Unpublish(content2); ResetEvents(); @@ -1766,7 +1778,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Move(content1, content2.Id); @@ -1853,10 +1865,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services IContent content2 = CreateContent(); Assert.IsNotNull(content2); - ContentService.SaveAndPublish(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); IContent content3 = CreateContent(content2.Id); Assert.IsNotNull(content3); - ContentService.SaveAndPublish(content3); + ContentService.Publish(content3, content3.AvailableCultures.ToArray()); ContentService.Unpublish(content2); ContentService.Move(content1, content3.Id); @@ -1921,7 +1933,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); IContent copy = ContentService.Copy(content, Constants.System.Root, false); @@ -1941,7 +1953,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); IContent content2 = CreateContent(); Assert.IsNotNull(content2); ContentService.Move(content, content2.Id); @@ -1964,7 +1976,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateBranch(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); ResetEvents(); IContent copy = ContentService.Copy(content, Constants.System.Root, false); @@ -2008,15 +2020,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { IContent content = CreateContent(); Assert.IsNotNull(content); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); int v1 = content.VersionId; content.Properties.First().SetValue("changed"); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); int v2 = content.VersionId; content.Properties.First().SetValue("again"); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); int v3 = content.VersionId; Console.WriteLine(v1); @@ -2052,11 +2066,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsFalse(content.WasPropertyDirty("Published")); - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsTrue(content.WasPropertyDirty("Published")); // has just been published - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); Assert.IsFalse(content.IsPropertyDirty("Published")); Assert.IsFalse(content.WasPropertyDirty("Published")); // was published already } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs new file mode 100644 index 0000000000..e0a22889d8 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs @@ -0,0 +1,503 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +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.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public partial class ContentPublishingServiceTests +{ + [Test] + public async Task Can_Publish_Root() + { + VerifyIsNotPublished(Textpage.Key); + + var result = await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsPublished(Textpage.Key); + } + + [Test] + public async Task Publish_Single_Item_Does_Not_Publish_Children() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + + VerifyIsPublished(Textpage.Key); + VerifyIsNotPublished(Subpage.Key); + } + + [Test] + public async Task Can_Publish_Child_Of_Root() + { + await ContentPublishingService.PublishAsync(Textpage.Key, new[] { "*" }, Constants.Security.SuperUserKey); + + var result = await ContentPublishingService.PublishAsync(Subpage.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsPublished(Subpage.Key); + } + + [TestCase(true)] + [TestCase(false)] + public async Task Publish_Branch_Does_Not_Publish_Unpublished_Children_Unless_Explicitly_Instructed_To(bool force) + { + var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, force, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + + VerifyIsPublished(Textpage.Key); + + if (force) + { + Assert.AreEqual(4, result.Result.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Textpage.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage2.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage3.Key]); + VerifyIsPublished(Subpage.Key); + VerifyIsPublished(Subpage2.Key); + VerifyIsPublished(Subpage3.Key); + } + else + { + Assert.AreEqual(1, result.Result.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Textpage.Key]); + VerifyIsNotPublished(Subpage.Key); + VerifyIsNotPublished(Subpage2.Key); + VerifyIsNotPublished(Subpage3.Key); + } + } + + [Test] + public async Task Can_Publish_Branch_Beneath_Root() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + var subpage2Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 2-2", Subpage2.Id); + ContentService.Save(subpage2Subpage, -1); + + VerifyIsNotPublished(Subpage2.Key); + var result = await ContentPublishingService.PublishBranchAsync(Subpage2.Key, _allCultures, true, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(2, result.Result.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage2.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[subpage2Subpage.Key]); + VerifyIsPublished(Subpage2.Key); + VerifyIsPublished(subpage2Subpage.Key); + VerifyIsNotPublished(Subpage.Key); + } + + [Test] + public async Task Can_Cancel_Publishing_With_Notification() + { + ContentNotificationHandler.PublishingContent = notification => notification.Cancel = true; + + var result = await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.CancelledByEvent, result.Result); + } + + [Test] + public async Task Can_Publish_Variant_Content() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(2, content.PublishedCultures.Count()); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langEn.IsoCode)); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langDa.IsoCode)); + } + + [Test] + public async Task Can_Publish_All_Variants_And_Unpublish_All_Variants_And_Publish_A_Single_Variant() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(2, content.PublishedCultures.Count()); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langEn.IsoCode)); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langDa.IsoCode)); + + result = await ContentPublishingService.UnpublishAsync(content.Key, "*", Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(2, content.PublishedCultures.Count()); + + result = await ContentPublishingService.PublishAsync(content.Key, new[] { langDa.IsoCode }, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + // FIXME: when work item 32809 has been fixed, this should assert for 1 expected published cultures + Assert.AreEqual(2, content.PublishedCultures.Count()); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langDa.IsoCode)); + } + + [Test] + public async Task Can_Publish_Branch_Of_Variant_Content() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + root.SetValue("title", "EN root title", culture: langEn.IsoCode); + root.SetValue("title", "DA root title", culture: langDa.IsoCode); + ContentService.Save(root); + + IContent child = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN child") + .WithCultureName(langDa.IsoCode, "DA child") + .WithParent(root) + .Build(); + child.SetValue("title", "EN child title", culture: langEn.IsoCode); + child.SetValue("title", "DA child title", culture: langDa.IsoCode); + ContentService.Save(child); + + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, true, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(2, result.Result!.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[root.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[child.Key]); + + root = ContentService.GetById(root.Key)!; + Assert.AreEqual(2, root.PublishedCultures.Count()); + Assert.IsTrue(root.PublishedCultures.InvariantContains(langEn.IsoCode)); + Assert.IsTrue(root.PublishedCultures.InvariantContains(langDa.IsoCode)); + + child = ContentService.GetById(child.Key)!; + Assert.AreEqual(2, child.PublishedCultures.Count()); + Assert.IsTrue(child.PublishedCultures.InvariantContains(langEn.IsoCode)); + Assert.IsTrue(child.PublishedCultures.InvariantContains(langDa.IsoCode)); + } + + [Test] + public async Task Can_Publish_Culture_With_Other_Culture_Invalid() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode }, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(1, content.PublishedCultures.Count()); + Assert.IsTrue(content.PublishedCultures.First().InvariantEquals(langEn.IsoCode)); + } + + [Test] + public async Task Can_Publish_Culture_Branch_With_Other_Culture_Invalid() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + root.SetValue("title", "EN title", culture: langEn.IsoCode); + root.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(root); + + IContent child = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN child") + .WithCultureName(langDa.IsoCode, "DA child") + .WithParent(root) + .Build(); + child.SetValue("title", "EN child title", culture: langEn.IsoCode); + child.SetValue("title", "DA child title", culture: langDa.IsoCode); + ContentService.Save(child); + + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, true, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(2, result.Result!.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[root.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[child.Key]); + + root = ContentService.GetById(root.Key)!; + Assert.AreEqual(1, root.PublishedCultures.Count()); + Assert.IsTrue(root.PublishedCultures.InvariantContains(langEn.IsoCode)); + + child = ContentService.GetById(child.Key)!; + Assert.AreEqual(1, child.PublishedCultures.Count()); + Assert.IsTrue(child.PublishedCultures.InvariantContains(langEn.IsoCode)); + } + + [Test] + public async Task Can_Publish_Culture_Branch_Without_Other_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + root.SetValue("title", "EN title", culture: langEn.IsoCode); + root.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(root); + + root = ContentService.GetById(root.Key)!; + + IContent child = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN child") + .WithCultureName(langDa.IsoCode, "DA child") + .WithParent(root) + .Build(); + child.SetValue("title", "EN child title", culture: langEn.IsoCode); + child.SetValue("title", "DA child title", culture: langDa.IsoCode); + ContentService.Save(child); + + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode }, true, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(2, result.Result!.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[root.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[child.Key]); + + root = ContentService.GetById(root.Key)!; + Assert.AreEqual(1, root.PublishedCultures.Count()); + Assert.IsTrue(root.PublishedCultures.InvariantContains(langEn.IsoCode)); + + child = ContentService.GetById(child.Key)!; + Assert.AreEqual(1, child.PublishedCultures.Count()); + Assert.IsTrue(child.PublishedCultures.InvariantContains(langEn.IsoCode)); + } + + [Test] + public async Task Cannot_Publish_Variant_Content_With_Mandatory_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(true); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(2, content.PublishedCultures.Count()); + } + + [Test] + public async Task Cannot_Publish_Non_Existing_Content() + { + var result = await ContentPublishingService.PublishAsync(Guid.NewGuid(), _allCultures, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.ContentNotFound, result.Result); + } + + [Test] + public async Task Cannot_Publish_Branch_Of_Non_Existing_Content() + { + var key = Guid.NewGuid(); + var result = await ContentPublishingService.PublishBranchAsync(key, _allCultures, true, Constants.Security.SuperUserKey); + Assert.IsFalse(result); + Assert.AreEqual(ContentPublishingOperationStatus.ContentNotFound, result.Result[key]); + } + + [Test] + public async Task Cannot_Publish_Invalid_Content() + { + var content = await CreateInvalidContent(); + + var result = await ContentPublishingService.PublishAsync(content.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsFalse(result); + Assert.AreEqual(ContentPublishingOperationStatus.ContentInvalid, result.Result); + VerifyIsNotPublished(content.Key); + } + + [Test] + public async Task Cannot_Publish_Branch_With_Invalid_Parent() + { + var content = await CreateInvalidContent(Textpage); + var child = ContentBuilder.CreateSimpleContent(ContentType, "Child page", content.Id); + ContentService.Save(child, -1); + Assert.AreEqual(content.Id, ContentService.GetById(child.Key)!.ParentId); + + var result = await ContentPublishingService.PublishBranchAsync(Textpage.Key, _allCultures, true, Constants.Security.SuperUserKey); + + Assert.IsFalse(result); + Assert.AreEqual(5, result.Result.Count); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Textpage.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage2.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result[Subpage3.Key]); + Assert.AreEqual(ContentPublishingOperationStatus.ContentInvalid, result.Result[content.Key]); + VerifyIsPublished(Textpage.Key); + VerifyIsPublished(Subpage.Key); + VerifyIsPublished(Subpage2.Key); + VerifyIsPublished(Subpage3.Key); + VerifyIsNotPublished(content.Key); + VerifyIsNotPublished(child.Key); + } + + [Test] + public async Task Cannot_Publish_Invalid_Variant_Content() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.ContentInvalid, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(0, content.PublishedCultures.Count()); + } + + [Test] + public async Task Cannot_Publish_Variant_Content_Without_Mandatory_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(true); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, new[] { langDa.IsoCode }, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.MandatoryCultureMissing, result.Result); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(0, content.PublishedCultures.Count()); + } + + [Test] + public async Task Cannot_Publish_Culture_Branch_With_Invalid_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent root = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + root.SetValue("title", "EN title", culture: langEn.IsoCode); + root.SetValue("title", null, culture: langDa.IsoCode); + ContentService.Save(root); + + IContent child = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN child") + .WithCultureName(langDa.IsoCode, "DA child") + .WithParent(root) + .Build(); + child.SetValue("title", "EN child title", culture: langEn.IsoCode); + child.SetValue("title", "DA child title", culture: langDa.IsoCode); + ContentService.Save(child); + + var result = await ContentPublishingService.PublishBranchAsync(root.Key, new[] { langEn.IsoCode, langDa.IsoCode }, true, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + Assert.AreEqual(1, result.Result!.Count); + Assert.AreEqual(ContentPublishingOperationStatus.ContentInvalid, result.Result[root.Key]); + + root = ContentService.GetById(root.Key)!; + Assert.AreEqual(0, root.PublishedCultures.Count()); + + child = ContentService.GetById(child.Key)!; + Assert.AreEqual(0, child.PublishedCultures.Count()); + } + + [Test] + public async Task Cannot_Publish_Child_Of_Unpublished_Parent() + { + VerifyIsNotPublished(Textpage.Key); + + var result = await ContentPublishingService.PublishAsync(Subpage.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.PathNotPublished, result.Result); + VerifyIsNotPublished(Subpage.Key); + } + + [Test] + public async Task Cannot_Publish_From_Trash() + { + ContentService.MoveToRecycleBin(Subpage); + Assert.IsTrue(ContentService.GetById(Subpage.Key)!.Trashed); + + var result = await ContentPublishingService.PublishAsync(Subpage.Key, _allCultures, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.InTrash, result.Result); + VerifyIsNotPublished(Subpage.Key); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs new file mode 100644 index 0000000000..f8e3f9a6ae --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Unpublish.cs @@ -0,0 +1,184 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +public partial class ContentPublishingServiceTests +{ + [Test] + public async Task Can_Unpublish_Root() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + VerifyIsPublished(Textpage.Key); + + 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_Child() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Subpage.Key, _allCultures, Constants.Security.SuperUserKey); + VerifyIsPublished(Textpage.Key); + VerifyIsPublished(Subpage.Key); + + var result = await ContentPublishingService.UnpublishAsync(Subpage.Key, null, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsPublished(Textpage.Key); + VerifyIsNotPublished(Subpage.Key); + } + + [Test] + public async Task Can_Unpublish_Structure() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Subpage.Key, _allCultures, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Subpage2.Key, _allCultures, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Subpage3.Key, _allCultures, Constants.Security.SuperUserKey); + VerifyIsPublished(Textpage.Key); + VerifyIsPublished(Subpage.Key); + VerifyIsPublished(Subpage2.Key); + VerifyIsPublished(Subpage3.Key); + + var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsNotPublished(Textpage.Key); + // the sub pages are still published... + VerifyIsPublished(Subpage.Key); + VerifyIsPublished(Subpage2.Key); + VerifyIsPublished(Subpage3.Key); + // ... but should no longer be routable because the parent is unpublished + Assert.IsFalse(ContentService.IsPathPublished(Subpage)); + Assert.IsFalse(ContentService.IsPathPublished(Subpage2)); + Assert.IsFalse(ContentService.IsPathPublished(Subpage3)); + } + + [Test] + public async Task Can_Unpublish_Unpublished_Content() + { + VerifyIsNotPublished(Textpage.Key); + + 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_Cancel_Unpublishing_With_Notification() + { + await ContentPublishingService.PublishAsync(Textpage.Key, _allCultures, Constants.Security.SuperUserKey); + VerifyIsPublished(Textpage.Key); + + ContentNotificationHandler.UnpublishingContent = notification => notification.Cancel = true; + + var result = await ContentPublishingService.UnpublishAsync(Textpage.Key, null, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.CancelledByEvent, result.Result); + VerifyIsPublished(Textpage.Key); + } + + [Test] + public async Task Can_Unpublish_Single_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + VerifyIsPublished(content.Key); + + var result = await ContentPublishingService.UnpublishAsync(content.Key, langEn.IsoCode, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsPublished(content.Key); + + content = ContentService.GetById(content.Key)!; + Assert.AreEqual(1, content.PublishedCultures.Count()); + Assert.IsTrue(content.PublishedCultures.InvariantContains(langDa.IsoCode)); + } + + [Test] + public async Task Can_Unpublish_All_Cultures() + { + var (langEn, langDa, contentType) = await SetupVariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + VerifyIsPublished(content.Key); + + var result = await ContentPublishingService.UnpublishAsync(content.Key, null, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsNotPublished(content.Key); + + content = ContentService.GetById(content.Key)!; + // FIXME: when work item 32809 has been fixed, this should assert for 0 expected published cultures + Assert.AreEqual(2, content.PublishedCultures.Count()); + } + + [Test] + public async Task Can_Unpublish_All_Cultures_By_Unpublishing_Mandatory_Culture() + { + var (langEn, langDa, contentType) = await SetupVariantTest(true); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName(langEn.IsoCode, "EN root") + .WithCultureName(langDa.IsoCode, "DA root") + .Build(); + content.SetValue("title", "EN title", culture: langEn.IsoCode); + content.SetValue("title", "DA title", culture: langDa.IsoCode); + ContentService.Save(content); + await ContentPublishingService.PublishAsync(content.Key, new[] { langEn.IsoCode, langDa.IsoCode }, Constants.Security.SuperUserKey); + VerifyIsPublished(content.Key); + + var result = await ContentPublishingService.UnpublishAsync(content.Key, langEn.IsoCode, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsNotPublished(content.Key); + + content = ContentService.GetById(content.Key)!; + // FIXME: when work item 32809 has been fixed, this should assert for 0 expected published cultures + Assert.AreEqual(2, content.PublishedCultures.Count()); + } + + [Test] + public async Task Can_Unpublish_From_Trash() + { + ContentService.MoveToRecycleBin(Subpage); + Assert.IsTrue(ContentService.GetById(Subpage.Key)!.Trashed); + + var result = await ContentPublishingService.UnpublishAsync(Subpage.Key, null, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Result); + VerifyIsNotPublished(Subpage.Key); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs new file mode 100644 index 0000000000..9a2163b3bb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs @@ -0,0 +1,120 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public partial class ContentPublishingServiceTests : UmbracoIntegrationTestWithContent +{ + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private static readonly string[] _allCultures = new[] { "*" }; + + [SetUp] + public void SetupTest() + { + ContentNotificationHandler.PublishingContent = null; + ContentNotificationHandler.UnpublishingContent = null; + } + + private async Task CreateInvalidContent(IContent? parent = null) + { + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + // create a new content type and allow the default content type as child + var contentType = ContentTypeBuilder.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type", mandatoryProperties: true, defaultTemplateId: template.Id); + contentType.AllowedAsRoot = true; + contentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; + ContentTypeService.Save(contentType); + + // allow the new content type as child to the default content type + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(contentType.Key, 0, contentType.Alias) }; + ContentTypeService.Save(ContentType); + + var content = ContentBuilder.CreateSimpleContent(contentType, "Invalid Content", parent?.Id ?? Constants.System.Root); + content.SetValue("author", string.Empty); + ContentService.Save(content); + + return content; + } + + private async Task<(ILanguage LangEn, ILanguage LangDa, IContentType contentType)> SetupVariantTest(bool englishIsMandatoryLanguage = false) + { + var langEn = (await LanguageService.GetAsync("en-US"))!; + if (englishIsMandatoryLanguage) + { + langEn.IsMandatory = true; + await LanguageService.UpdateAsync(langEn, Constants.Security.SuperUserKey); + } + + var langDa = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey); + + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var key = Guid.NewGuid(); + var contentType = new ContentTypeBuilder() + .WithAlias("variantContent") + .WithName("Variant Content") + .WithKey(key) + .WithContentVariation(ContentVariation.Culture) + .AddAllowedContentType() + .WithKey(key) + .WithAlias("variantContent") + .Done() + .AddPropertyGroup() + .WithAlias("content") + .WithName("Content") + .WithSupportsPublishing(true) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithVariations(ContentVariation.Culture) + .WithMandatory(true) + .Done() + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + await ContentTypeService.SaveAsync(contentType, Constants.Security.SuperUserKey); + + return (langEn, langDa, contentType); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + => builder + .AddNotificationHandler() + .AddNotificationHandler(); + + private void VerifyIsPublished(Guid key) => Assert.IsTrue(ContentService.GetById(key)!.Published); + + private void VerifyIsNotPublished(Guid key) => Assert.IsFalse(ContentService.GetById(key)!.Published); + + private ILanguageService LanguageService => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private class ContentNotificationHandler : INotificationHandler, INotificationHandler + { + public static Action? PublishingContent { get; set; } + + public static Action? UnpublishingContent { get; set; } + + public void Handle(ContentPublishingNotification notification) => PublishingContent?.Invoke(notification); + + public void Handle(ContentUnpublishingNotification notification) => UnpublishingContent?.Invoke(notification); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs index 52a39a8caa..63fafd7716 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceNotificationTests.cs @@ -229,7 +229,7 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest try { - ContentService.SaveAndPublish(document, "fr-FR"); + ContentService.Publish(document, new[] { "fr-FR" }); Assert.IsTrue(publishingWasCalled); Assert.IsTrue(publishedWasCalled); } @@ -253,6 +253,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest var savingWasCalled = false; var savedWasCalled = false; + var publishingWasCalled = false; + var publishedWasCalled = false; ContentNotificationHandler.SavingContent = notification => { @@ -275,21 +277,50 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest var propValue = saved.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null); Assert.AreEqual("title", propValue.EditedValue); - Assert.AreEqual("title", propValue.PublishedValue); + Assert.AreEqual(null, propValue.PublishedValue); savedWasCalled = true; }; + ContentNotificationHandler.PublishingContent = notification => + { + var publishing = notification.PublishedEntities.First(); + + Assert.AreEqual("title", publishing.GetValue("title")); + + publishingWasCalled = true; + }; + + ContentNotificationHandler.PublishedContent = notification => + { + var published = notification.PublishedEntities.First(); + + Assert.AreSame("title", document.GetValue("title")); + + // We're only dealing with invariant here. + var propValue = published.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null); + + Assert.AreEqual("title", propValue.EditedValue); + Assert.AreEqual("title", propValue.PublishedValue); + + publishedWasCalled = true; + }; + try { - ContentService.SaveAndPublish(document); + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); Assert.IsTrue(savingWasCalled); Assert.IsTrue(savedWasCalled); + Assert.IsTrue(publishingWasCalled); + Assert.IsTrue(publishedWasCalled); } finally { ContentNotificationHandler.SavingContent = null; ContentNotificationHandler.SavedContent = null; + ContentNotificationHandler.PublishingContent = null; + ContentNotificationHandler.PublishedContent = null; } } @@ -302,7 +333,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest IContent document = new Content("content", -1, _contentType); - var result = ContentService.SaveAndPublish(document); + ContentService.Save(document); + var result = ContentService.Publish(document, document.AvailableCultures.ToArray()); Assert.IsFalse(result.Success); Assert.AreEqual("title", result.InvalidProperties.First().Alias); @@ -325,7 +357,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest try { - result = ContentService.SaveAndPublish(document); + ContentService.Save(document); + result = ContentService.Publish(document, document.AvailableCultures.ToArray()); Assert.IsTrue(result .Success); // will succeed now because we were able to specify the required value in the Saving event Assert.IsTrue(savingWasCalled); @@ -353,7 +386,8 @@ public class ContentServiceNotificationTests : UmbracoIntegrationTest IContent document = new Content("content", -1, _contentType); document.SetCultureName("hello", "en-US"); document.SetCultureName("bonjour", "fr-FR"); - ContentService.SaveAndPublish(document); + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); Assert.IsTrue(document.IsCulturePublished("fr-FR")); Assert.IsTrue(document.IsCulturePublished("en-US")); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs index 2f74c6c979..64064ce7b7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePublishBranchTests.cs @@ -26,8 +26,9 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest private IContentTypeService ContentTypeService => GetRequiredService(); - [TestCase(1)] // use overload w/ culture: "*" - [TestCase(2)] // use overload w/ cultures: new [] { "*" } + [TestCase(1)] // publish w/ culture: content.AvailableCultures.ToArray() + [TestCase(2)] // publish w/ cultures: new [] { "*" } + [TestCase(3)] // publish w/ cultures: Array.Empty() [LongRunning] public void Can_Publish_Invariant_Branch(int method) { @@ -49,27 +50,29 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // !force = publishes those that are actually published, and have changes // here: root (root is always published) - var r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + var r = PublishInvariantBranch(iRoot, false, method).ToArray(); // not forcing, ii1 and ii2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "iroot"); AssertPublishResults(r, x => x.Result, PublishResultType.SuccessPublish); // prepare - ContentService.SaveAndPublish(iRoot); - ContentService.SaveAndPublish(ii1); + ContentService.Publish(iRoot, iRoot.AvailableCultures.ToArray()); + ContentService.Publish(ii1, ii1.AvailableCultures.ToArray()); IContent ii11 = new Content("ii11", ii1, iContentType); ii11.SetValue("ip", "vii11"); - ContentService.SaveAndPublish(ii11); + ContentService.Save(ii11); + ContentService.Publish(ii11, ii11.AvailableCultures.ToArray()); IContent ii12 = new Content("ii12", ii1, iContentType); ii11.SetValue("ip", "vii12"); ContentService.Save(ii12); - ContentService.SaveAndPublish(ii2); + ContentService.Publish(ii2, ii2.AvailableCultures.ToArray()); IContent ii21 = new Content("ii21", ii2, iContentType); ii21.SetValue("ip", "vii21"); - ContentService.SaveAndPublish(ii21); + ContentService.Save(ii21); + ContentService.Publish(ii21, ii21.AvailableCultures.ToArray()); IContent ii22 = new Content("ii22", ii2, iContentType); ii22.SetValue("ip", "vii22"); ContentService.Save(ii22); @@ -85,7 +88,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // !force = publishes those that are actually published, and have changes // here: nothing - r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + r = PublishInvariantBranch(iRoot, false, method).ToArray(); // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); @@ -114,7 +117,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // here: iroot and ii11 // not forcing, ii12 and ii2, ii21, ii22 not published yet: only root, ii1, ii11 got published - r = SaveAndPublishInvariantBranch(iRoot, false, method).ToArray(); + r = PublishInvariantBranch(iRoot, false, method).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "ii11"); AssertPublishResults( r, @@ -125,7 +128,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // force = publishes everything that has changes // here: ii12, ii2, ii22 - ii21 was published already but masked - r = SaveAndPublishInvariantBranch(iRoot, true, method).ToArray(); + r = PublishInvariantBranch(iRoot, true, method).ToArray(); AssertPublishResults( r, x => x.Content.Name, @@ -165,7 +168,8 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest vRoot.SetValue("vp", "vroot.de", "de"); vRoot.SetValue("vp", "vroot.ru", "ru"); vRoot.SetValue("vp", "vroot.es", "es"); - ContentService.SaveAndPublish(vRoot); + ContentService.Save(vRoot); + ContentService.Publish(vRoot, vRoot.AvailableCultures.ToArray()); // create/publish child IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); @@ -176,13 +180,14 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "iv1.de", "de"); iv1.SetValue("vp", "iv1.ru", "ru"); iv1.SetValue("vp", "iv1.es", "es"); - ContentService.SaveAndPublish(iv1); + ContentService.Save(iv1); + ContentService.Publish(iv1, iv1.AvailableCultures.ToArray()); // update the child iv1.SetValue("vp", "UPDATED-iv1.de", "de"); ContentService.Save(iv1); - var r = ContentService.SaveAndPublishBranch(vRoot, false) + var r = ContentService.PublishBranch(vRoot, false, vRoot.AvailableCultures.ToArray()) .ToArray(); // no culture specified so "*" is used, so all cultures Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); @@ -202,7 +207,8 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest vRoot.SetValue("vp", "vroot.de", "de"); vRoot.SetValue("vp", "vroot.ru", "ru"); vRoot.SetValue("vp", "vroot.es", "es"); - ContentService.SaveAndPublish(vRoot); + ContentService.Save(vRoot); + ContentService.Publish(vRoot, vRoot.AvailableCultures.ToArray()); // create/publish child IContent iv1 = new Content("iv1", vRoot, vContentType, "de"); @@ -213,13 +219,14 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest iv1.SetValue("vp", "iv1.de", "de"); iv1.SetValue("vp", "iv1.ru", "ru"); iv1.SetValue("vp", "iv1.es", "es"); - ContentService.SaveAndPublish(iv1); + ContentService.Save(iv1); + ContentService.Publish(iv1, iv1.AvailableCultures.ToArray()); // update the child iv1.SetValue("vp", "UPDATED-iv1.de", "de"); var saveResult = ContentService.Save(iv1); - var r = ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + var r = ContentService.PublishBranch(vRoot, false, new [] { "de" }).ToArray(); Assert.AreEqual(PublishResultType.SuccessPublishAlready, r[0].Result); Assert.AreEqual(PublishResultType.SuccessPublishCulture, r[1].Result); } @@ -265,7 +272,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // !force = publishes those that are actually published, and have changes // here: nothing - var r = ContentService.SaveAndPublishBranch(vRoot, false).ToArray(); // no culture specified = all cultures + var r = ContentService.PublishBranch(vRoot, false, new[] { "*" }).ToArray(); // no culture specified = all cultures // not forcing, iv1 and iv2 not published yet: only root got published AssertPublishResults(r, x => x.Content.Name, "vroot.de"); @@ -278,7 +285,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest vRoot.SetValue("vp", "changed.es", "es"); ContentService.Save(vRoot); // now root has drafts in all cultures - ContentService.SaveAndPublish(iv1, new[] { "de", "ru" }); // now iv1 de and ru are published + ContentService.Publish(iv1, new[] { "de", "ru" }); // now iv1 de and ru are published iv1.SetValue("ip", "changed"); iv1.SetValue("vp", "changed.de", "de"); @@ -298,7 +305,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest Assert.IsTrue(iv1.IsCulturePublished("ru")); Assert.IsFalse(iv1.IsCulturePublished("es")); - r = ContentService.SaveAndPublishBranch(vRoot, false, "de").ToArray(); + r = ContentService.PublishBranch(vRoot, false, new[] { "de" }).ToArray(); // not forcing, iv2 not published yet: only root and iv1 got published AssertPublishResults(r, x => x.Content.Name, "vroot.de", "iv1.de"); @@ -345,10 +352,12 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest // invariant root -> invariant -> variant iRoot = new Content("iroot", -1, iContentType); iRoot.SetValue("ip", "iroot"); - ContentService.SaveAndPublish(iRoot); + ContentService.Save(iRoot); + ContentService.Publish(iRoot, iRoot.AvailableCultures.ToArray()); ii1 = new Content("ii1", iRoot, iContentType); ii1.SetValue("ip", "vii1"); - ContentService.SaveAndPublish(ii1); + ContentService.Save(ii1); + ContentService.Publish(ii1, ii1.AvailableCultures.ToArray()); ii1.SetValue("ip", "changed"); ContentService.Save(ii1); iv11 = new Content("iv11.de", ii1, vContentType, "de"); @@ -359,7 +368,8 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest ContentService.Save(iv11); iv11.SetCultureName("iv11.ru", "ru"); - var xxx = ContentService.SaveAndPublish(iv11, new[] { "de", "ru" }); + ContentService.Save(iv11); + var xxx = ContentService.Publish(iv11, new[] { "de", "ru" }); Assert.AreEqual("iv11.de", iv11.GetValue("vp", "de", published: true)); Assert.AreEqual("iv11.ru", iv11.GetValue("vp", "ru", published: true)); @@ -375,7 +385,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.SaveAndPublishBranch(iRoot, false, "de").ToArray(); + var r = ContentService.PublishBranch(iRoot, false, new[] { "de" }).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -401,7 +411,7 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest { Can_Publish_Mixed_Branch(out var iRoot, out var ii1, out var iv11); - var r = ContentService.SaveAndPublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); + var r = ContentService.PublishBranch(iRoot, false, new[] { "de", "ru" }).ToArray(); AssertPublishResults(r, x => x.Content.Name, "iroot", "ii1", "iv11.de"); AssertPublishResults( r, @@ -479,16 +489,18 @@ public class ContentServicePublishBranchTests : UmbracoIntegrationTest ContentTypeService.Save(vContentType); } - private IEnumerable SaveAndPublishInvariantBranch(IContent content, bool force, int method) + private IEnumerable PublishInvariantBranch(IContent content, bool force, int method) { // ReSharper disable RedundantArgumentDefaultValue // ReSharper disable ArgumentsStyleOther switch (method) { case 1: - return ContentService.SaveAndPublishBranch(content, force, "*"); + return ContentService.PublishBranch(content, force, content.AvailableCultures.ToArray()); case 2: - return ContentService.SaveAndPublishBranch(content, force, cultures: new[] { "*" }); + return ContentService.PublishBranch(content, force, cultures: new[] { "*" }); + case 3: + return ContentService.PublishBranch(content, force, cultures: Array.Empty()); default: throw new ArgumentOutOfRangeException(nameof(method)); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index f331b5cc62..56b5340c1a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -64,7 +64,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest IContent content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); content1 = ContentService.GetById(content1.Id); @@ -111,7 +112,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); content1 = ContentService.GetById(content1.Id); @@ -162,7 +164,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest IContent content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); contentType.Variations = ContentVariation.Culture; ContentTypeService.Save(contentType); @@ -245,7 +248,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); contentType.Variations = ContentVariation.Nothing; ContentTypeService.Save(contentType); @@ -304,7 +308,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); IContent content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2"); content2.SetCultureName("name-fr", "fr-FR"); @@ -313,7 +318,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); //// pretend we already have invariant values // using (var scope = ScopeProvider.CreateScope()) @@ -349,7 +355,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest new[] { "hello", "world", "some", "tags", "plus" }, culture: "fr-FR"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "another", "one" }, culture: "en-US"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); propertyType.Variations = ContentVariation.Nothing; ContentTypeService.Save(contentType); @@ -411,7 +418,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest frValue, culture: "fr-FR"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", enValue, culture: "en-US"); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); propertyType.Variations = ContentVariation.Nothing; ContentTypeService.Save(contentType); @@ -438,12 +446,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags", "plus" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); var content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2"); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); // verify var tags = TagService.GetTagsForEntity(content1.Id); @@ -468,12 +478,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags", "bam" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); var content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2"); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); // verify var tags = TagService.GetTagsForEntity(content1.Id); @@ -500,12 +512,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags", "plus" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); var content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2", content1.Id); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, Array.Empty()); // verify var tags = TagService.GetTagsForEntity(content1.Id); @@ -539,8 +553,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest allTags = TagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content1.PublishCulture(CultureImpact.Invariant); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); Assert.IsTrue(content1.Published); @@ -575,12 +589,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags", "bam" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, content1.AvailableCultures.ToArray()); var content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2"); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, content2.AvailableCultures.ToArray()); ContentService.Unpublish(content1); ContentService.Unpublish(content2); @@ -601,12 +617,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags", "bam" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); var content2 = ContentBuilder.CreateSimpleContent(contentType, "Tagged content 2", content1); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, Array.Empty()); ContentService.Unpublish(content1); @@ -621,8 +639,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var allTags = TagService.GetAllContentTags(); Assert.AreEqual(0, allTags.Count()); - content1.PublishCulture(CultureImpact.Invariant); - ContentService.SaveAndPublish(content1); + ContentService.Publish(content1, Array.Empty()); tags = TagService.GetTagsForEntity(content2.Id); Assert.AreEqual(4, tags.Count()); @@ -669,7 +686,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest ContentService.Save(child2); // Act - ContentService.SaveAndPublishBranch(content, true); + ContentService.PublishBranch(content, true, content.AvailableCultures.ToArray()); // Assert var propertyTypeId = contentType.PropertyTypes.Single(x => x.Alias == "tags").Id; @@ -708,7 +725,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content"); content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // edit tags and save content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "another", "world" }, @@ -746,7 +764,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest // Act content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); // Assert Assert.AreEqual(4, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); @@ -775,12 +794,14 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content"); content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); // Act content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "another", "world" }, true); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); // Assert Assert.AreEqual(5, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); @@ -809,11 +830,13 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content"); content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello", "world", "some", "tags" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); // Act content.RemoveTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "some", "world" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); // Assert Assert.AreEqual(2, content.Properties["tags"].GetValue().ToString().Split(',').Distinct().Count()); @@ -855,7 +878,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello,world,tags", "new" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // Act content = ContentService.GetById(content.Id); @@ -898,7 +922,8 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "hello,world,tags", "new" }); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, content.AvailableCultures.ToArray()); // Act content = ContentService.GetById(content.Id); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceTests.cs index f5f21b8d53..8b01dcf8cf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceTests.cs @@ -107,7 +107,7 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest var contentItem = ContentBuilder.CreateSimpleContent(contentType, "MyName_" + index + "_" + i, parentId); ContentService.Save(contentItem); - ContentService.SaveAndPublish(contentItem); + ContentService.Publish(contentItem, new[] { "*" }); parentId = contentItem.Id; ids.Add(contentItem.Id); @@ -161,7 +161,7 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest var contentItem = ContentBuilder.CreateSimpleContent(contentType, "MyName_" + index + "_" + i, parentId); ContentService.Save(contentItem); - ContentService.SaveAndPublish(contentItem); + ContentService.Publish(contentItem, new[] { "*" }); parentId = contentItem.Id; } } @@ -203,17 +203,17 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest var root = ContentBuilder.CreateSimpleContent(contentType1, "Root"); ContentService.Save(root); - ContentService.SaveAndPublish(root); + ContentService.Publish(root, new[] { "*" }); var level1 = ContentBuilder.CreateSimpleContent(contentType2, "L1", root.Id); ContentService.Save(level1); - ContentService.SaveAndPublish(level1); + ContentService.Publish(level1, new[] { "*" }); for (var i = 0; i < 2; i++) { var level3 = ContentBuilder.CreateSimpleContent(contentType3, "L2" + i, level1.Id); ContentService.Save(level3); - ContentService.SaveAndPublish(level3); + ContentService.Publish(level3, new[] { "*" }); } ContentTypeService.Delete(contentType1); @@ -247,7 +247,8 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest FileService.SaveTemplate(contentType1.DefaultTemplate); ContentTypeService.Save(contentType1); IContent contentItem = ContentBuilder.CreateTextpageContent(contentType1, "Testing", -1); - ContentService.SaveAndPublish(contentItem); + ContentService.Save(contentItem); + ContentService.Publish(contentItem, new[] { "*" }); var initProps = contentItem.Properties.Count; // remove a property @@ -355,7 +356,8 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest // Act var homeDoc = cs.Create("Home Page", -1, contentTypeAlias); - cs.SaveAndPublish(homeDoc); + cs.Save(homeDoc); + cs.Publish(homeDoc, new[] { "*" }); // Assert Assert.That(ctBase.HasIdentity, Is.True); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index 57808b2981..a21d9686ba 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -833,7 +833,8 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest document.SetCultureName("doc1fr", "fr"); document.SetValue("value1", "v1en-init", "en"); document.SetValue("value1", "v1fr-init", "fr"); - ContentService.SaveAndPublish(document); // all values are published which means the document is not 'edited' + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); // all values are published which means the document is not 'edited' document = ContentService.GetById(document.Id); Assert.IsFalse(document.IsCultureEdited("en")); @@ -877,7 +878,8 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest // update the invariant value and publish document.SetValue("value1", "v1inv"); - ContentService.SaveAndPublish(document); + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); document = ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); @@ -918,7 +920,8 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest // publish again document.SetValue("value1", "v1en2", "en"); // update the value now that it's variant again document.SetValue("value1", "v1fr2", "fr"); // update the value now that it's variant again - ContentService.SaveAndPublish(document); + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); document = ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); @@ -956,7 +959,8 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest document.SetCultureName("doc1en", "en"); document.SetCultureName("doc1fr", "fr"); document.SetValue("value1", "v1en-init"); - ContentService.SaveAndPublish(document); // all values are published which means the document is not 'edited' + ContentService.Save(document); // all values are published which means the document is not 'edited' + ContentService.Publish(document, document.AvailableCultures.ToArray()); document = ContentService.GetById(document.Id); Assert.IsFalse(document.IsCultureEdited("en")); @@ -990,7 +994,8 @@ public class ContentTypeServiceVariantsTests : UmbracoIntegrationTest // update the culture value and publish document.SetValue("value1", "v1en2", "en"); - ContentService.SaveAndPublish(document); + ContentService.Save(document); + ContentService.Publish(document, document.AvailableCultures.ToArray()); document = ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs index e553d6165d..ac5e9d6128 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentVersionCleanupServiceTest.cs @@ -47,11 +47,12 @@ internal class ContentVersionCleanupServiceTest : UmbracoIntegrationTest ContentTypeService.Save(contentTypeA); var content = ContentBuilder.CreateSimpleContent(contentTypeA); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); for (var i = 0; i < 10; i++) { - ContentService.SaveAndPublish(content); + ContentService.Publish(content, Array.Empty()); } var before = GetReport(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs index f9d981933d..6ad3b202ae 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/NuCacheRebuildTests.cs @@ -35,7 +35,8 @@ public class NuCacheRebuildTests : UmbracoIntegrationTest var content = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); var cachedContent = ContentService.GetById(content.Id); var segment = urlSegmentProvider.GetUrlSegment(cachedContent); @@ -69,7 +70,8 @@ public class NuCacheRebuildTests : UmbracoIntegrationTest Assert.AreEqual("hello", segment); - ContentService.SaveAndPublish(content); + ContentService.Save(content); + ContentService.Publish(content, Array.Empty()); cachedContent = ContentService.GetById(content.Id); segment = urlSegmentProvider.GetUrlSegment(cachedContent); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TagServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TagServiceTests.cs index 3224e4964b..f622e0ff30 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TagServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TagServiceTests.cs @@ -63,18 +63,22 @@ public class TagServiceTests : UmbracoIntegrationTest IContent content1 = ContentBuilder.CreateSimpleContent(_contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "cow", "pig", "goat" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); // change content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "elephant" }, true); content1.RemoveTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "cow" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); // more changes content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "mouse" }, true); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); content1.RemoveTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "mouse" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); // get it back content1 = ContentService.GetById(content1.Id); @@ -101,15 +105,18 @@ public class TagServiceTests : UmbracoIntegrationTest var content1 = ContentBuilder.CreateSimpleContent(_contentType, "Tagged content 1"); content1.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "cow", "pig", "goat" }); - ContentService.SaveAndPublish(content1); + ContentService.Save(content1); + ContentService.Publish(content1, Array.Empty()); var content2 = ContentBuilder.CreateSimpleContent(_contentType, "Tagged content 2"); content2.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "cow", "pig" }); - ContentService.SaveAndPublish(content2); + ContentService.Save(content2); + ContentService.Publish(content2, Array.Empty()); var content3 = ContentBuilder.CreateSimpleContent(_contentType, "Tagged content 3"); content3.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", new[] { "cow" }); - ContentService.SaveAndPublish(content3); + ContentService.Save(content3); + ContentService.Publish(content3, Array.Empty()); // Act var tags = TagService.GetAllContentTags() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 4839c74dfd..91f40cd221 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -75,6 +75,12 @@ ContentEditingServiceTests.cs + + ContentPublishingServiceTests.cs + + + ContentPublishingServiceTests.cs + UserServiceCrudTests.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs index 3741136a0c..4ed9697b6c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs @@ -66,7 +66,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); var model = new ContentItemSaveBuilder() .WithContent(content) @@ -125,7 +126,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); var model = new ContentItemSaveBuilder() .WithContent(content) @@ -196,7 +198,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); var model = new ContentItemSaveBuilder() .WithId(content.Id) @@ -262,7 +265,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); var model = new ContentItemSaveBuilder() .WithContent(content) .Build(); @@ -324,7 +328,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, Array.Empty()); content.Name = null; // Removes the name of one of the variants to force an error var model = new ContentItemSaveBuilder() @@ -390,7 +395,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithKeyValue("title", "Cool invariant title") .Done() .Build(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, content.AvailableCultures.ToArray()); content.CultureInfos[0].Name = null; // Removes the name of one of the variants to force an error var model = new ContentItemSaveBuilder() @@ -496,7 +502,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .Build(); var contentService = GetRequiredService(); - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, content.AvailableCultures.ToArray()); var childContent = new ContentBuilder() .WithoutIdentity() @@ -506,7 +513,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithCultureName(UsIso, "Child") .Build(); - contentService.SaveAndPublish(childContent); + contentService.Save(childContent); + contentService.Publish(childContent, content.AvailableCultures.ToArray()); var grandChildContent = new ContentBuilder() .WithoutIdentity() @@ -654,7 +662,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .Build(); var contentService = GetRequiredService(); - contentService.SaveAndPublish(rootNode); + contentService.Save(rootNode); + contentService.Publish(rootNode, rootNode.AvailableCultures.ToArray()); var childNode = new ContentBuilder() .WithoutIdentity() @@ -664,7 +673,8 @@ public class ContentControllerTests : UmbracoTestServerTestBase .WithCultureName(UsIso, "Child") .Build(); - contentService.SaveAndPublish(childNode); + contentService.Save(childNode); + contentService.Publish(childNode, childNode.AvailableCultures.ToArray()); var grandChild = new ContentBuilder() .WithoutIdentity() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs index 0dc9e55a28..85bdaf1159 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/EntityControllerTests.cs @@ -223,14 +223,16 @@ public class EntityControllerTests : UmbracoTestServerTestBase .WithContentType(contentType); var root = builder.WithName("foo").Build(); - contentService.SaveAndPublish(root); + contentService.Save(root); + contentService.Publish(root, root.AvailableCultures.ToArray()); contentItems.Add(builder.WithParent(root).WithName("bar").Build()); contentItems.Add(builder.WithParent(root).WithName("baz").Build()); foreach (var content in contentItems) { - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, content.AvailableCultures.ToArray()); } } @@ -273,14 +275,16 @@ public class EntityControllerTests : UmbracoTestServerTestBase .WithContentType(contentType); var root = builder.WithName("foo").Build(); - contentService.SaveAndPublish(root); + contentService.Save(root); + contentService.Publish(root, root.AvailableCultures.ToArray()); contentItems.Add(builder.WithParent(root).WithName("bar").Build()); contentItems.Add(builder.WithParent(root).WithName("baz").Build()); foreach (var content in contentItems) { - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, content.AvailableCultures.ToArray()); } } @@ -323,14 +327,16 @@ public class EntityControllerTests : UmbracoTestServerTestBase .WithContentType(contentType); var root = builder.WithName("foo").Build(); - contentService.SaveAndPublish(root); + contentService.Save(root); + contentService.Publish(root, root.AvailableCultures.ToArray()); contentItems.Add(builder.WithParent(root).WithName("bar").Build()); contentItems.Add(builder.WithParent(root).WithName("baz").Build()); foreach (var content in contentItems) { - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, content.AvailableCultures.ToArray()); } } @@ -373,14 +379,16 @@ public class EntityControllerTests : UmbracoTestServerTestBase .WithContentType(contentType); var root = builder.WithName("foo").Build(); - contentService.SaveAndPublish(root); + contentService.Save(root); + contentService.Publish(root, root.AvailableCultures.ToArray()); contentItems.Add(builder.WithParent(root).WithName("bar").Build()); contentItems.Add(builder.WithParent(root).WithName("baz").Build()); foreach (var content in contentItems) { - contentService.SaveAndPublish(content); + contentService.Save(content); + contentService.Publish(content, root.AvailableCultures.ToArray()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 420ffa9c32..82a29d0b0a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -27,7 +27,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest InstallationSummary = packagingService.InstallCompiledPackageData(xml); Root = InstallationSummary.ContentInstalled.First(); - ContentService.SaveAndPublish(Root); + ContentService.Publish(Root, Root.AvailableCultures.ToArray()); var cultures = new List { @@ -336,7 +336,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest public async Task Cannot_Assign_Already_Used_Domains() { var copy = ContentService.Copy(Root, Root.ParentId, false); - ContentService.SaveAndPublish(copy!); + ContentService.Publish(copy!, copy!.AvailableCultures.ToArray()); var domainService = GetRequiredService(); var updateModel = new DomainsUpdateModel diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index ca2ae76428..672427697f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -574,6 +574,44 @@ public class VariationTests prop.PublishValues(); } + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void NoValueTests(bool variesByCulture, bool variesBySegment) + { + var variation = variesByCulture && variesBySegment + ? ContentVariation.CultureAndSegment + : variesByCulture + ? ContentVariation.Culture + : variesBySegment + ? ContentVariation.Segment + : ContentVariation.Nothing; + + var culture = variesByCulture ? "en-US" : null; + var segment = variesBySegment ? "my-segment" : null; + + var propertyType = new PropertyTypeBuilder() + .WithAlias("prop") + .WithSupportsPublishing(true) + .WithVariations(variation) + .Build(); + + var prop = new Property(propertyType); + var propertyValidationService = GetPropertyValidationService(); + + // "no value" is valid for non-mandatory properties + Assert.IsTrue(propertyValidationService.IsPropertyValid(prop, culture, segment)); + + propertyType.Mandatory = true; + + // "no value" is NOT valid for mandatory properties + Assert.IsFalse(propertyValidationService.IsPropertyValid(prop, culture, segment)); + + // can publish, even though invalid + prop.PublishValues(); + } + private static Content CreateContent(IContentType contentType, int id = 1, string name = "content") => new ContentBuilder() .WithId(id)