Avoid hybrid cache usage when traversing unpublished ancestors in a published context (non preview) (#19137)
* Filter Available should not return items without published ancestors when not in preview * Update unittests mocks * Internal documentation and minor code tidy. * Tidied up integration tests and added new tests for the added method. --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
@@ -1,16 +1,29 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Verifies the published status of documents.
|
||||
/// </summary>
|
||||
public interface IPublishStatusQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a document is published in a specific culture.
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The document's key.</param>
|
||||
/// <param name="culture">The culture.</param>
|
||||
/// <returns>True if document is published in the specified culture.</returns>
|
||||
bool IsDocumentPublished(Guid documentKey, string culture);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a document is published in any culture.
|
||||
/// </summary>
|
||||
/// <param name="documentKey">Key to check for.</param>
|
||||
/// <param name="documentKey">The document's key.</param>
|
||||
/// <returns>True if document has any published culture.</returns>
|
||||
bool IsDocumentPublishedInAnyCulture(Guid documentKey) => IsDocumentPublished(documentKey, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies if a document has a published ancestor path (i.e. all ancestors are themselves published in at least one culture).
|
||||
/// </summary>
|
||||
/// <param name="documentKey">The document's key.</param>
|
||||
/// <returns>True if document has a published ancestor path.</returns>
|
||||
bool HasPublishedAncestorPath(Guid documentKey);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
@@ -7,38 +6,75 @@ using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IPublishStatusManagementService" /> and <see cref="IPublishStatusQueryService" /> verifying the published
|
||||
/// status of documents.
|
||||
/// </summary>
|
||||
public class PublishStatusService : IPublishStatusManagementService, IPublishStatusQueryService
|
||||
{
|
||||
private readonly ILogger<PublishStatusService> _logger;
|
||||
private readonly IPublishStatusRepository _publishStatusRepository;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly ILanguageService _languageService;
|
||||
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
|
||||
|
||||
private readonly IDictionary<Guid, ISet<string>> _publishedCultures = new Dictionary<Guid, ISet<string>>();
|
||||
|
||||
private string? DefaultCulture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishStatusService"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 17.")]
|
||||
public PublishStatusService(
|
||||
ILogger<PublishStatusService> logger,
|
||||
IPublishStatusRepository publishStatusRepository,
|
||||
ICoreScopeProvider coreScopeProvider)
|
||||
: this(logger, publishStatusRepository, coreScopeProvider, StaticServiceProvider.Instance.GetRequiredService<ILanguageService>())
|
||||
: this(
|
||||
logger,
|
||||
publishStatusRepository,
|
||||
coreScopeProvider,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILanguageService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishStatusService"/> class.
|
||||
/// </summary>
|
||||
[Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 17.")]
|
||||
public PublishStatusService(
|
||||
ILogger<PublishStatusService> logger,
|
||||
IPublishStatusRepository publishStatusRepository,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
ILanguageService languageService)
|
||||
: this(
|
||||
logger,
|
||||
publishStatusRepository,
|
||||
coreScopeProvider,
|
||||
languageService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationQueryService>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PublishStatusService"/> class.
|
||||
/// </summary>
|
||||
public PublishStatusService(
|
||||
ILogger<PublishStatusService> logger,
|
||||
IPublishStatusRepository publishStatusRepository,
|
||||
ICoreScopeProvider coreScopeProvider,
|
||||
ILanguageService languageService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_publishStatusRepository = publishStatusRepository;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_languageService = languageService;
|
||||
_documentNavigationQueryService = documentNavigationQueryService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_publishedCultures.Clear();
|
||||
@@ -60,6 +96,7 @@ public class PublishStatusService : IPublishStatusManagementService, IPublishSta
|
||||
DefaultCulture = await _languageService.GetDefaultIsoCodeAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsDocumentPublished(Guid documentKey, string culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(culture) && DefaultCulture is not null)
|
||||
@@ -88,6 +125,31 @@ public class PublishStatusService : IPublishStatusManagementService, IPublishSta
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasPublishedAncestorPath(Guid contentKey)
|
||||
{
|
||||
var success = _documentNavigationQueryService.TryGetAncestorsKeys(contentKey, out IEnumerable<Guid> keys);
|
||||
if (success is false)
|
||||
{
|
||||
// This might happen is certain cases, since notifications are not ordered, for instance, if you save and publish a content node in the same scope.
|
||||
// In this case we'll try and update the node in the cache even though it hasn't been updated in the document navigation cache yet.
|
||||
// It's okay to just return false here, since the node will be loaded later when it's actually requested.
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (Guid key in keys)
|
||||
{
|
||||
|
||||
if (IsDocumentPublishedInAnyCulture(key) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task AddOrUpdateStatusAsync(Guid documentKey, CancellationToken cancellationToken)
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
@@ -96,12 +158,14 @@ public class PublishStatusService : IPublishStatusManagementService, IPublishSta
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task RemoveAsync(Guid documentKey, CancellationToken cancellationToken)
|
||||
{
|
||||
_publishedCultures.Remove(documentKey);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task AddOrUpdateStatusWithDescendantsAsync(Guid rootDocumentKey, CancellationToken cancellationToken)
|
||||
{
|
||||
IDictionary<Guid, ISet<string>> publishStatus;
|
||||
|
||||
@@ -36,7 +36,9 @@ internal sealed class PublishedContentStatusFilteringService : IPublishedContent
|
||||
var preview = _previewService.IsInPreview();
|
||||
candidateKeys = preview
|
||||
? candidateKeysAsArray
|
||||
: candidateKeysAsArray.Where(key => _publishStatusQueryService.IsDocumentPublished(key, culture));
|
||||
: candidateKeysAsArray.Where(key =>
|
||||
_publishStatusQueryService.IsDocumentPublished(key, culture)
|
||||
&& _publishStatusQueryService.HasPublishedAncestorPath(key));
|
||||
|
||||
return WhereIsInvariantOrHasCulture(candidateKeys, culture, preview).ToArray();
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
// When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
|
||||
// Similarly, when a branch is published, next time the content is requested, the parent will be published,
|
||||
// this works because we don't cache null values.
|
||||
if (preview is false && contentCacheNode is not null && HasPublishedAncestorPath(contentCacheNode.Key) is false)
|
||||
if (preview is false && contentCacheNode is not null && _publishStatusQueryService.HasPublishedAncestorPath(contentCacheNode.Key) is false)
|
||||
{
|
||||
// Careful not to early return here. We need to complete the scope even if returning null.
|
||||
contentCacheNode = null;
|
||||
@@ -137,28 +137,6 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
return _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);
|
||||
}
|
||||
|
||||
private bool HasPublishedAncestorPath(Guid contentKey)
|
||||
{
|
||||
var success = _documentNavigationQueryService.TryGetAncestorsKeys(contentKey, out IEnumerable<Guid> keys);
|
||||
if (success is false)
|
||||
{
|
||||
// This might happen is certain cases, since 0notifications are not ordered, for instance, if you save and publish a content node in the same scope.
|
||||
// In this case we'll try and update the node in the cache even though it hasn't been updated in the document navigation cache yet.
|
||||
// It's okay to just return false here, since the node will be loaded later when it's actually requested.
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (Guid key in keys)
|
||||
{
|
||||
if (_publishStatusQueryService.IsDocumentPublishedInAnyCulture(key) is false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool GetPreview() => _previewService.IsInPreview();
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByContentType(IPublishedContentType contentType)
|
||||
@@ -191,7 +169,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
}
|
||||
|
||||
ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
|
||||
if (publishedNode is not null && HasPublishedAncestorPath(publishedNode.Key))
|
||||
if (publishedNode is not null && _publishStatusQueryService.HasPublishedAncestorPath(publishedNode.Key))
|
||||
{
|
||||
await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false), GenerateTags(key));
|
||||
}
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)]
|
||||
internal sealed class PublishStatusServiceTest : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
protected IPublishStatusQueryService PublishStatusQueryService => GetRequiredService<IPublishStatusQueryService>();
|
||||
|
||||
private const string DefaultCulture = "en-US";
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
|
||||
builder.AddNotificationHandler<ContentTreeChangeNotification, ContentTreeChangeDistributedCacheNotificationHandler>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InitializeAsync_loads_from_db()
|
||||
{
|
||||
var randomCulture = "da-DK";
|
||||
var sut = new PublishStatusService(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>());
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, randomCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, randomCulture));
|
||||
});
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.InitializeAsync(CancellationToken.None);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, randomCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, randomCulture));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddOrUpdateStatusWithDescendantsAsync()
|
||||
{
|
||||
var randomCulture = "da-DK";
|
||||
var sut = new PublishStatusService(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<ILanguageService>()
|
||||
);
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.AddOrUpdateStatusWithDescendantsAsync(Textpage.Key, CancellationToken.None);
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Updated due to being an descendant
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture)); // Do not exist
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture)); // Do not exist
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddOrUpdateStatusAsync()
|
||||
{
|
||||
var randomCulture = "da-DK";
|
||||
var sut = new PublishStatusService(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<ILanguageService>());
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.AddOrUpdateStatusAsync(Textpage.Key, CancellationToken.None);
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Not updated
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture)); // Do not exist
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture)); // Do not exist
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void When_Nothing_is_publised_all_return_false()
|
||||
{
|
||||
var randomCulture = "da-DK";
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, randomCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, randomCulture));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Unpublish_leads_to_unpublised_in_this_service()
|
||||
{
|
||||
var grandchild = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild", Subpage2.Id);
|
||||
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(grandchild, -1, contentSchedule);
|
||||
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
var randomCulture = "da-DK";
|
||||
|
||||
var subPage2FromDB = ContentService.GetById(Subpage2.Key);
|
||||
var publishResult = ContentService.Unpublish(subPage2FromDB);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(publishResult.Success);
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, DefaultCulture)); // grandchild is still published, but it will not be routable
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, randomCulture));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void When_Branch_is_publised_default_language_return_true()
|
||||
{
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
var randomCulture = "da-DK";
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, randomCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, randomCulture));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
internal sealed partial class PublishStatusServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task InitializeAsync_Loads_From_Database()
|
||||
{
|
||||
var sut = CreatePublishedStatusService();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, UnusedCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, UnusedCulture));
|
||||
});
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.InitializeAsync(CancellationToken.None);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x => x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, UnusedCulture));
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, UnusedCulture));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddOrUpdateStatusWithDescendantsAsync_Updates_Document_Path_Published_Status()
|
||||
{
|
||||
var sut = new PublishStatusService(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<ILanguageService>(),
|
||||
GetRequiredService<IDocumentNavigationQueryService>());
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.AddOrUpdateStatusWithDescendantsAsync(Textpage.Key, CancellationToken.None);
|
||||
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Updated due to being an descendant
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, UnusedCulture)); // Do not exist
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, UnusedCulture)); // Do not exist
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddOrUpdateStatusAsync_Updates_Document_Published_Status()
|
||||
{
|
||||
var sut = new PublishStatusService(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<ILanguageService>(),
|
||||
GetRequiredService<IDocumentNavigationQueryService>());
|
||||
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
|
||||
// Act
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
await sut.AddOrUpdateStatusAsync(Textpage.Key, CancellationToken.None);
|
||||
|
||||
Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Not updated
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, UnusedCulture)); // Do not exist
|
||||
Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, UnusedCulture)); // Do not exist
|
||||
}
|
||||
|
||||
private PublishStatusService CreatePublishedStatusService()
|
||||
=> new(
|
||||
GetRequiredService<ILogger<PublishStatusService>>(),
|
||||
GetRequiredService<IPublishStatusRepository>(),
|
||||
GetRequiredService<ICoreScopeProvider>(),
|
||||
GetRequiredService<ILanguageService>(),
|
||||
GetRequiredService<IDocumentNavigationQueryService>());
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
internal sealed partial class PublishStatusServiceTests
|
||||
{
|
||||
private IPublishStatusQueryService PublishStatusQueryService => GetRequiredService<IPublishStatusQueryService>();
|
||||
|
||||
[Test]
|
||||
public void When_Nothing_Is_Publised_All_Documents_Have_Unpublished_Status()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, UnusedCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, UnusedCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Textpage.Key));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage.Key));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage2.Key));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage3.Key));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Unpublish_Updates_Document_Path_Published_Status()
|
||||
{
|
||||
var grandchild = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild", Subpage2.Id);
|
||||
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(grandchild, -1, contentSchedule);
|
||||
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
|
||||
var subPage2FromDB = ContentService.GetById(Subpage2.Key);
|
||||
var publishResult = ContentService.Unpublish(subPage2FromDB);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x => x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(publishResult.Success);
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, DefaultCulture)); // grandchild is still published, but it will not be routable
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, UnusedCulture));
|
||||
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Textpage.Key));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage2.Key));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(grandchild.Key));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Publish_Branch_Updates_Document_Path_Published_Status()
|
||||
{
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(publishResults.All(x => x.Result == PublishResultType.SuccessPublish));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, UnusedCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, UnusedCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, UnusedCulture));
|
||||
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Textpage.Key));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage.Key));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage2.Key));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublishedInAnyCulture(Subpage3.Key));
|
||||
|
||||
Assert.IsTrue(PublishStatusQueryService.HasPublishedAncestorPath(Textpage.Key));
|
||||
Assert.IsTrue(PublishStatusQueryService.HasPublishedAncestorPath(Subpage.Key));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Published_Document_With_UnPublished_Parent_Has_Unpublished_Path()
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
});
|
||||
|
||||
ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
});
|
||||
|
||||
ContentService.Unpublish(Textpage);
|
||||
|
||||
// Unpublish the root item - the sub page will still be published but it won't have a published path.
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
|
||||
Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
|
||||
|
||||
Assert.IsFalse(PublishStatusQueryService.HasPublishedAncestorPath(Subpage.Key));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)]
|
||||
internal sealed partial class PublishStatusServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
private const string DefaultCulture = "en-US";
|
||||
private const string UnusedCulture = "da-DK";
|
||||
|
||||
protected override void CustomTestSetup(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
|
||||
builder.AddNotificationHandler<ContentTreeChangeNotification, ContentTreeChangeDistributedCacheNotificationHandler>();
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,12 @@
|
||||
<Compile Update="Umbraco.Core\Services\PublishedUrlInfoProviderTests.cs">
|
||||
<DependentUpon>PublishedUrlInfoProviderTestsBase.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Core\Services\PublishStatusServiceTests.Management.cs">
|
||||
<DependentUpon>PublishStatusServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Core\Services\PublishStatusServiceTests.Query.cs">
|
||||
<DependentUpon>PublishStatusServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceTests.ChildUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@@ -65,6 +65,9 @@ public class DeliveryApiTests
|
||||
publishStatusQueryService
|
||||
.Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
|
||||
.Returns(true);
|
||||
publishStatusQueryService
|
||||
.Setup(x => x.HasPublishedAncestorPath(It.IsAny<Guid>()))
|
||||
.Returns(true);
|
||||
|
||||
PublishStatusQueryService = publishStatusQueryService.Object;
|
||||
}
|
||||
|
||||
@@ -329,6 +329,9 @@ public partial class PublishedContentStatusFilteringServiceTests
|
||||
.TryGetValue(key, out var item)
|
||||
&& idIsPublished(item.Id)
|
||||
&& (item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture)));
|
||||
publishStatusQueryService
|
||||
.Setup(s => s.HasPublishedAncestorPath(It.IsAny<Guid>()))
|
||||
.Returns(true);
|
||||
return publishStatusQueryService.Object;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user