From e342e795dd92262032970839e57d1af0339463cd Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 24 Sep 2024 09:39:23 +0200 Subject: [PATCH] V15: Cache Seeding (#17102) * Update to dotnet 9 and update nuget packages * Update umbraco code version * Update Directory.Build.props Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Include preview version in pipeline * update template projects * update global json with specific version * Update version.json to v15 * Rename TrimStart and TrimEnd to string specific * Rename to Exact * Update global.json Co-authored-by: Ronald Barendse * Remove includePreviewVersion * Rename to trim exact * Add new Hybridcache project * Add tests * Start implementing PublishedContent.cs * Implement repository for content * Refactor to use async everywhere * Add cache refresher * make public as needed for serialization * Use content type cache to get content type out * Refactor to use ContentCacheNode model, that goes in the memory cache * Remove content node kit as its not needed * Implement tests for ensuring caching * Implement better asserts * Implement published property * Refactor to use mapping * Rename to document tests * Update to test properties * Create more tests * Refactor mock tests into own file * Update property test * Fix published version of content * Change default cache level to elements * Refactor to always have draft * Refactor to not use PublishedModelFactory * Added tests * Added and updated tests * Fixed tests * Don't return empty object with id * More tests * Added key * Another key * Refactor CacheService to be responsible for using the hybrid cache * Use notification handler to remove deleted content from cache * Add more tests for missing functions * Implement missing methods * Remove HasContent as it pertains to routing * Fik up test * formatting * refactor variable names * Implement variant tests * Map all the published content properties * Get item out of cache first, to assert updated * Implement member cache * Add member test * Implement media cache * Implement property tests for media tests * Refactor tests to use extension method * Add more media tests * Refactor properties to no longer have element caching * Don't use property cache level * Start implementing seeding * Only seed when main * Add Immutable for performance * Implement permanent seeding of content * Implement cache settings * Implement tests for seeding * Update package version * start refactoring nurepo * Refactor so draft & published nodes are cached individually * Refactor RefreshContent to take node instead of IContent * Refactor media to also use cache nodes * Remove member from repo as it isn't cached * Refactor media to not include preview, as media has no draft * create new benchmark project * POC Integration benchmarks with custom api controllers * Start implementing content picker tests * Implement domain cache * Rework content cache to implement interface * Start implementing elements cache * Implement published snapshot service * Publish snapshot tests * Use snapshot for elements cache * Create test proving we don't clear cache when updating content picker * Clear entire elements cache * Remove properties from element cache, when content gets updated. * Rename methods to async * Refactor to use old cache interfaces instead of new ones * Remove snapshot, as it is no longer needed * Fix tests building * Refactor domaincache to not have snapshots * Delete benchmarks * Delete benchmarks * Add HybridCacheProject to Umbraco * Add comment to route value transformer * Implement is draft * remove snapshot from property * V15 updated the hybrid caching integration tests to use ContentEditingService (#16947) * Added builder extension withParentKey * Created builder with ContentEditingService * Added usage of the ContentEditingService to SETUP * Started using ContentEditingService builder in tests * Updated builder extensions * Fixed builder * Clean up * Clean up, not done * Added Ids * Remove entries from cache on delete * Fix up seeding logic * Don't register hybrid cache twice * Change seeded entry options * Update hybrid cache package * Fix up published property to work with delivery api again * Fix dependency injection to work with tests * Fix naming * Dont make caches nullable * Make content node sealed * Remove path and other unused from content node * Remove hacky 2 phase ctor * Refactor to actually set content templates * Remove umbraco context * Remove "HasBy" methods * rename property data * Delete obsolete legacy stuff * Add todo for making expiration configurable * Add todo in UmbracoContext * Add clarifying comment in content factory * Remove xml stuff from published property * Fix according to review * Make content type cache injectible * Make content type cache injectible * Rename to database cache repository * Rename to document cache * Add TODO * Refactor to async * Rename to async * Make everything async * Remove duplicate line from json schema * Move Hybrid cache project * Remove leftover file * Refactor to use keys * Refactor published content to no longer have content data, as it is on the node itself * Refactor to member to use proper content node ctor * Move tests to own folder * Add immutable objects to property and content data for performance * Make property data public * Fix member caching to be singleton * Obsolete GetContentType * Remove todo * Fix naming * Fix lots of exposed errors due to scope test * Add final scope tests * Rename to document cache service * Rename test files * Create new doc type tests * Add ignore to tests * Start implementing refresh for content type save * Clear contenttype cache when contenttype is updated * Fix test Teh contenttype is not upated unless the property is dirty * Updated tests * Added tests * Use init for ContentSourceDto * Startup of setup * Fix get by key in PublishedContentTypeCache * Remove ContentType from PublishedContentTypeCache when contenttype is deleted * Created interfaces for the builder with the necessary properties * Created builder for PropertyTypeContainer * Created builder for PropertyTypeEditing * Created builder for PropertyTypeValidationEditing * Made adjustments to the builder * Updated name of usage * Commented out to test * Cleaned up builders * Updated integration test setup * Moved tests * Added interface * Add IDocumentSeedKeyProvider and migrate existing logic to seed key provider * Added functionality to the INavigationQueryService to get root keys * Fixed issue with navigation * Created helper to Convert a IContentType to ContentTypeUpdateModel * Added interfaces * Added builder * Cleaned up builders and added fixes * Added tests for PublishedContentTypeCache * Applied changes in builder * Add BreadthFirstKeyProvider * Use ISet for seedkey providers * Implement GetContentSource by key * Seed the cache with keys provided by seed key providers * Builder updates * Test setup updates * Updated tests * Dont require contenttype keys for seeding * Fix cache settings * Don't inject cache settings into SeedingNotificationHandler * Fix tests * Use enlistment for setting updated cache item * Pin seeded nodes for longer * Fix BreadthFirstKeyProvider * Fix ContentTypeSeedKeyProvider * Fix tests * Only seed published documents * Only cache published if contentCacheNode is not draft * Fix incorrect templateId * Removed unnecessary setup * initialized value * Fixed template test * Removed test * Updated tests * Removed code that was not used * Removed unused cacheSettings * Re-organize to support media cache seeding * Add MediaBreadthFirstKeyProvider * Seed media * Don't use IdKeyMap when removing content from cache * Don't clear IdKeyMap in DocumentCacheService * Add unit tests * Don't use IdKeyMap when deleting media * Add default value to timespan * Use cancellation tokens when doing loop * Fixed Models Builder error --------- Co-authored-by: Zeegaan Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Ronald Barendse Co-authored-by: Andreas Zerbst Co-authored-by: Sven Geusens Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Constants-SqlTemplates.cs | 1 + .../Factories/NavigationFactory.cs | 7 +- src/Umbraco.Core/Models/CacheSettings.cs | 18 +- .../ContentNavigationServiceBase.cs | 13 +- .../Navigation/INavigationQueryService.cs | 1 + .../UmbracoBuilderExtensions.cs | 13 + .../Factories/CacheNodeFactory.cs | 2 +- .../IDocumentSeedKeyProvider.cs | 6 + .../IMediaSeedKeyProvider.cs | 6 + .../ISeedKeyProvider.cs | 10 + .../CacheRefreshingNotificationHandler.cs | 4 +- .../SeedingNotificationHandler.cs | 27 +- .../Persistence/DatabaseCacheRepository.cs | 83 +++++- .../Persistence/IDatabaseCacheRepository.cs | 12 + .../BreadthFirstKeyProvider.cs | 67 +++++ .../Document/ContentTypeSeedKeyProvider.cs | 32 +++ .../DocumentBreadthFirstKeyProvider.cs | 15 ++ .../Media/MediaBreadthFirstKeyProvider.cs | 14 + .../Services/DocumentCacheService.cs | 153 ++++++++--- .../Services/IDocumentCacheService.cs | 4 +- .../Services/IMediaCacheService.cs | 4 +- .../Services/MediaCacheService.cs | 75 +++++- .../Umbraco.PublishedCache.HybridCache.csproj | 3 + .../Builders/ContentEditingBuilder.cs | 53 ++-- .../Builders/ContentTypeEditingBuilder.cs | 240 ++++++++++++++++++ .../Builders/ContentTypeSortBuilder.cs | 5 + .../Builders/Extensions/BuilderExtensions.cs | 7 + .../Interfaces/IWIthContainerKeyBuilder.cs | 6 + .../Interfaces/IWithDataTypeKeyBuilder.cs | 6 + .../Builders/Interfaces/IWithLabelOnTop.cs | 6 + .../Interfaces/IWithMandatoryBuilder.cs | 6 + .../IWithMandatoryMessageBuilder.cs | 6 + .../IWithRegularExpressionBuilder.cs | 6 + .../IWithRegularExpressionMessage.cs | 6 + .../Builders/Interfaces/IWithTypeBuilder.cs | 6 + .../Interfaces/IWithVariesByCultureBuilder.cs | 6 + .../Interfaces/IWithVariesBySegmentBuilder.cs | 6 + .../Builders/PropertyTypeAppearanceBuilder.cs | 22 ++ .../Builders/PropertyTypeContainerBuilder.cs | 66 +++++ .../Builders/PropertyTypeEditingBuilder.cs | 176 +++++++++++++ .../PropertyTypeValidationEditingBuilder.cs | 55 ++++ .../TestHelpers/ContentTypeUpdateHelper.cs | 83 ++++++ ...mbracoIntegrationTestWithContentEditing.cs | 95 ++++--- .../Cache/PublishedContentTypeCacheTests.cs | 63 +++++ .../DocumentHybridCacheDocumentTypeTests.cs | 34 +-- .../DocumentHybridCacheMockTests.cs | 115 ++++++--- .../DocumentHybridCachePropertyTest.cs | 65 +++-- .../DocumentHybridCacheTemplateTests.cs | 44 ++++ .../DocumentHybridCacheTests.cs | 197 +++++++------- .../DocumentHybridCacheVariantsTests.cs | 115 ++++----- .../DocumentBreadthFirstKeyProviderTests.cs | 117 +++++++++ 51 files changed, 1769 insertions(+), 413 deletions(-) create mode 100644 src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index ad5b326035..3641510fd6 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -30,6 +30,7 @@ public static partial class Constants public static class NuCacheDatabaseDataSource { public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeKey = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeKey"; public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; public const string SourcesSelectUmbracoNodeJoin = diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs index 316c6031d6..815312e048 100644 --- a/src/Umbraco.Core/Factories/NavigationFactory.cs +++ b/src/Umbraco.Core/Factories/NavigationFactory.cs @@ -9,11 +9,10 @@ internal static class NavigationFactory /// /// Builds a dictionary of NavigationNode objects from a given dataset. /// + /// A dictionary of objects with key corresponding to their unique Guid. /// The objects used to build the navigation nodes dictionary. - /// A dictionary of objects with key corresponding to their unique Guid. - public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable entities) + public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure,IEnumerable entities) { - var nodesStructure = new ConcurrentDictionary(); var entityList = entities.ToList(); var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); @@ -39,7 +38,5 @@ internal static class NavigationFactory parentNode.AddChild(node); } } - - return nodesStructure; } } diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs index dcd7211347..2d4373a4da 100644 --- a/src/Umbraco.Core/Models/CacheSettings.cs +++ b/src/Umbraco.Core/Models/CacheSettings.cs @@ -1,13 +1,29 @@ -using Umbraco.Cms.Core.Configuration.Models; +using System.ComponentModel; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Core.Models; [UmbracoOptions(Constants.Configuration.ConfigCache)] public class CacheSettings { + internal const int StaticDocumentBreadthFirstSeedCount = 100; + + internal const int StaticMediaBreadthFirstSeedCount = 100; + internal const string StaticSeedCacheDuration = "365.00:00:00"; + /// /// Gets or sets a value for the collection of content type ids to always have in the cache. /// public List ContentTypeKeys { get; set; } = new(); + + [DefaultValue(StaticDocumentBreadthFirstSeedCount)] + public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount; + + + [DefaultValue(StaticMediaBreadthFirstSeedCount)] + public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount; + + [DefaultValue(StaticSeedCacheDuration)] + public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration); } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index e5755c8d87..394223c311 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -36,6 +36,9 @@ internal abstract class ContentNavigationServiceBase public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); + public bool TryGetRootKeys(out IEnumerable childrenKeys) + => TryGetRootKeysFromStructure(_navigationStructure, out childrenKeys); + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); @@ -162,6 +165,7 @@ internal abstract class ContentNavigationServiceBase _recycleBinNavigationStructure.TryRemove(key, out _); } + /// /// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed. /// Only relevant for items in the content and media trees (which have readLock values of -333 or -334). @@ -184,7 +188,7 @@ internal abstract class ContentNavigationServiceBase _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) : _navigationRepository.GetContentNodesByObjectType(objectTypeKey); - _navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels); + NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels); } private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey) @@ -213,6 +217,13 @@ internal abstract class ContentNavigationServiceBase return true; } + private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable childrenKeys) + { + // TODO can we make this more efficient? + childrenKeys = structure.Values.Where(x=>x.Parent is null).Select(x=>x.Key); + return true; + } + private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) { var descendants = new List(); diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 4e28f80bb6..9b6fb9807d 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -9,6 +9,7 @@ public interface INavigationQueryService bool TryGetParentKey(Guid childKey, out Guid? parentKey); bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); + bool TryGetRootKeys(out IEnumerable childrenKeys); bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs index 6ad695c154..984fcbe110 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -11,6 +11,8 @@ using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media; using Umbraco.Cms.Infrastructure.HybridCache.Serialization; using Umbraco.Cms.Infrastructure.HybridCache.Services; @@ -62,6 +64,17 @@ public static class UmbracoBuilderExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + builder.AddCacheSeeding(); + return builder; + } + + private static IUmbracoBuilder AddCacheSeeding(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + + builder.Services.AddSingleton(); return builder; } } diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs index 7fd91c4603..accc962c5c 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs @@ -17,7 +17,7 @@ internal class CacheNodeFactory : ICacheNodeFactory public ContentCacheNode ToContentCacheNode(IContent content, bool preview) { - ContentData contentData = GetContentData(content, !preview, preview ? content.PublishTemplateId : content.TemplateId); + ContentData contentData = GetContentData(content, !preview, preview ? content.TemplateId : content.PublishTemplateId); return new ContentCacheNode { Id = content.Id, diff --git a/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs new file mode 100644 index 0000000000..9fa39fd072 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface IDocumentSeedKeyProvider : ISeedKeyProvider +{ + +} diff --git a/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs new file mode 100644 index 0000000000..54ec4926fd --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface IMediaSeedKeyProvider : ISeedKeyProvider +{ + +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs new file mode 100644 index 0000000000..5883c89dc3 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface ISeedKeyProvider +{ + /// + /// Gets keys of documents that should be seeded into the cache. + /// + /// Keys to seed + ISet GetSeedKeys(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs index 105fad1d9d..a38c0408a1 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs @@ -51,7 +51,7 @@ internal sealed class CacheRefreshingNotificationHandler : foreach (IContent deletedEntity in notification.DeletedEntities) { await RefreshElementsCacheAsync(deletedEntity); - await _documentCacheService.DeleteItemAsync(deletedEntity.Id); + await _documentCacheService.DeleteItemAsync(deletedEntity); } } @@ -66,7 +66,7 @@ internal sealed class CacheRefreshingNotificationHandler : foreach (IMedia deletedEntity in notification.DeletedEntities) { await RefreshElementsCacheAsync(deletedEntity); - await _mediaCacheService.DeleteItemAsync(deletedEntity.Id); + await _mediaCacheService.DeleteItemAsync(deletedEntity); } } diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs index d0dfa76b67..1cea1a2360 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Options; +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.Infrastructure.HybridCache.Services; namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; @@ -9,13 +9,28 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; internal class SeedingNotificationHandler : INotificationAsyncHandler { private readonly IDocumentCacheService _documentCacheService; - private readonly CacheSettings _cacheSettings; + private readonly IMediaCacheService _mediaCacheService; + private readonly IRuntimeState _runtimeState; - public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IOptions cacheSettings) + public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IMediaCacheService mediaCacheService, IRuntimeState runtimeState) { _documentCacheService = documentCacheService; - _cacheSettings = cacheSettings.Value; + _mediaCacheService = mediaCacheService; + _runtimeState = runtimeState; } - public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken) => await _documentCacheService.SeedAsync(_cacheSettings.ContentTypeKeys); + public async Task HandleAsync(UmbracoApplicationStartedNotification notification, + CancellationToken cancellationToken) + { + + if (_runtimeState.Level <= RuntimeLevel.Install) + { + return; + } + + await Task.WhenAll( + _documentCacheService.SeedAsync(cancellationToken), + _mediaCacheService.SeedAsync(cancellationToken) + ); + } } diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs index d49d2f8799..0f917508aa 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs @@ -65,8 +65,13 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe { IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - // always refresh the edited data - await OnRepositoryRefreshed(serializer, contentCacheNode, true); + // We always cache draft and published separately, so we only want to cache drafts if the node is a draft type. + if (contentCacheNode.IsDraft) + { + await OnRepositoryRefreshed(serializer, contentCacheNode, true); + // if it's a draft node we don't need to worry about the published state + return; + } switch (publishedState) { @@ -208,11 +213,36 @@ AND cmsContentNu.nodeId IS NULL return CreateContentNodeKit(dto, serializer, preview); } - public IEnumerable GetContentByContentTypeKey(IEnumerable keys) + public async Task GetContentSourceAsync(Guid key, bool preview = false) { + Sql? sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeKey(SqlContext, key)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto == null) + { + return null; + } + + if (preview is false && dto.PubDataRaw is null && dto.PubData is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + return CreateContentNodeKit(dto, serializer, preview); + } + + private IEnumerable GetContentSourceByDocumentTypeKey(IEnumerable documentTypeKeys) + { + Guid[] keys = documentTypeKeys.ToArray(); if (keys.Any() is false) { - yield break; + return []; } Sql? sql = SqlContentSourcesSelect() @@ -222,17 +252,26 @@ AND cmsContentNu.nodeId IS NULL .WhereIn(x => x.UniqueId, keys,"n") .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + return GetContentNodeDtos(sql); + } + + public IEnumerable GetContentByContentTypeKey(IEnumerable keys) + { + IEnumerable dtos = GetContentSourceByDocumentTypeKey(keys); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - IEnumerable dtos = GetContentNodeDtos(sql); - foreach (ContentSourceDto row in dtos) { yield return CreateContentNodeKit(row, serializer, row.Published is false); } } + /// + public IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false) + => GetContentSourceByDocumentTypeKey(keys).Where(x => x.Published == published).Select(x => x.Key); + public async Task GetMediaSourceAsync(int id) { Sql? sql = SqlMediaSourcesSelect() @@ -252,6 +291,25 @@ AND cmsContentNu.nodeId IS NULL return CreateMediaNodeKit(dto, serializer); } + public async Task GetMediaSourceAsync(Guid key) + { + Sql? sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeKey(SqlContext, key)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); + } + private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview) { // use a custom SQL to update row version on each update @@ -642,6 +700,19 @@ WHERE cmsContentNu.nodeId IN ( return sql; } + private Sql SqlWhereNodeKey(ISqlContext sqlContext, Guid key) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeKey, + builder => + builder.Where(x => x.UniqueId == SqlTemplate.Arg("key"))); + + Sql sql = sqlTemplate.Sql(key); + return sql; + } + private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) { ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs index 47c18c07e1..6a88d5405e 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs @@ -8,10 +8,22 @@ internal interface IDatabaseCacheRepository Task GetContentSourceAsync(int id, bool preview = false); + Task GetContentSourceAsync(Guid key, bool preview = false); + Task GetMediaSourceAsync(int id); + Task GetMediaSourceAsync(Guid key); + + IEnumerable GetContentByContentTypeKey(IEnumerable keys); + /// + /// Gets all content keys of specific document types + /// + /// The document types to find content using. + /// The keys of all content use specific document types. + IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false); + /// /// Refreshes the nucache database row for the given cache node /> /// diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..99a5fe50a3 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs @@ -0,0 +1,67 @@ +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders; + +public abstract class BreadthFirstKeyProvider +{ + private readonly INavigationQueryService _navigationQueryService; + private readonly int _seedCount; + + public BreadthFirstKeyProvider(INavigationQueryService navigationQueryService, int seedCount) + { + _navigationQueryService = navigationQueryService; + _seedCount = seedCount; + } + + public ISet GetSeedKeys() + { + if (_seedCount == 0) + { + return new HashSet(); + } + + Queue keyQueue = new(); + HashSet keys = []; + int keyCount = 0; + + if (_navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys) is false) + { + return new HashSet(); + } + + foreach (Guid key in rootKeys) + { + keyCount++; + keys.Add(key); + keyQueue.Enqueue(key); + if (keyCount == _seedCount) + { + return keys; + } + } + + while (keyQueue.Count > 0 && keyCount < _seedCount) + { + Guid key = keyQueue.Dequeue(); + + if (_navigationQueryService.TryGetChildrenKeys(key, out IEnumerable childKeys) is false) + { + continue; + } + + foreach (Guid childKey in childKeys) + { + keys.Add(childKey); + keyCount++; + if (keyCount == _seedCount) + { + return keys; + } + + keyQueue.Enqueue(childKey); + } + } + + return keys; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs new file mode 100644 index 0000000000..bb0d721d63 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +internal sealed class ContentTypeSeedKeyProvider : IDocumentSeedKeyProvider +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDatabaseCacheRepository _databaseCacheRepository; + private readonly CacheSettings _cacheSettings; + + public ContentTypeSeedKeyProvider( + ICoreScopeProvider scopeProvider, + IDatabaseCacheRepository databaseCacheRepository, + IOptions cacheSettings) + { + _scopeProvider = scopeProvider; + _databaseCacheRepository = databaseCacheRepository; + _cacheSettings = cacheSettings.Value; + } + + public ISet GetSeedKeys() + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + var documentKeys = _databaseCacheRepository.GetContentKeysByContentTypeKeys(_cacheSettings.ContentTypeKeys, published: true).ToHashSet(); + scope.Complete(); + + return documentKeys; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..1e991d6277 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +internal sealed class DocumentBreadthFirstKeyProvider : BreadthFirstKeyProvider, IDocumentSeedKeyProvider +{ + public DocumentBreadthFirstKeyProvider( + IDocumentNavigationQueryService documentNavigationQueryService, + IOptions cacheSettings) + : base(documentNavigationQueryService, cacheSettings.Value.DocumentBreadthFirstSeedCount) + { + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..657ec4b8a0 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media; + +internal sealed class MediaBreadthFirstKeyProvider : BreadthFirstKeyProvider, IMediaSeedKeyProvider +{ + public MediaBreadthFirstKeyProvider( + IMediaNavigationQueryService navigationQueryService, IOptions cacheSettings) + : base(navigationQueryService, cacheSettings.Value.MediaBreadthFirstSeedCount) + { + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index b0aa936793..b91ea182f2 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Caching.Hybrid; +using System.Diagnostics; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -6,6 +9,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HybridCache.Services; @@ -17,7 +21,30 @@ internal sealed class DocumentCacheService : IDocumentCacheService private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; private readonly IPublishedContentFactory _publishedContentFactory; private readonly ICacheNodeFactory _cacheNodeFactory; + private readonly IEnumerable _seedKeyProviders; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly CacheSettings _cacheSettings; + private HashSet? _seedKeys; + private HashSet SeedKeys + { + get + { + if (_seedKeys is not null) + { + return _seedKeys; + } + + _seedKeys = []; + + foreach (IDocumentSeedKeyProvider provider in _seedKeyProviders) + { + _seedKeys.UnionWith(provider.GetSeedKeys()); + } + + return _seedKeys; + } + } public DocumentCacheService( IDatabaseCacheRepository databaseCacheRepository, @@ -25,7 +52,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService ICoreScopeProvider scopeProvider, Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, IPublishedContentFactory publishedContentFactory, - ICacheNodeFactory cacheNodeFactory) + ICacheNodeFactory cacheNodeFactory, + IEnumerable seedKeyProviders, + IOptions cacheSettings, + IPublishedModelFactory publishedModelFactory) { _databaseCacheRepository = databaseCacheRepository; _idKeyMap = idKeyMap; @@ -33,25 +63,21 @@ internal sealed class DocumentCacheService : IDocumentCacheService _hybridCache = hybridCache; _publishedContentFactory = publishedContentFactory; _cacheNodeFactory = cacheNodeFactory; + _seedKeyProviders = seedKeyProviders; + _publishedModelFactory = publishedModelFactory; + _cacheSettings = cacheSettings.Value; } - // TODO: Stop using IdKeyMap for these, but right now we both need key and id for caching.. public async Task GetByKeyAsync(Guid key, bool preview = false) { - Attempt idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document); - if (idAttempt.Success is false) - { - return null; - } - using ICoreScope scope = _scopeProvider.CreateCoreScope(); ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( GetCacheKey(key, preview), // Unique key to the cache entry - async cancel => await _databaseCacheRepository.GetContentSourceAsync(idAttempt.Result, preview)); + async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, preview)); scope.Complete(); - return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory); } public async Task GetByIdAsync(int id, bool preview = false) @@ -67,37 +93,55 @@ internal sealed class DocumentCacheService : IDocumentCacheService GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry async cancel => await _databaseCacheRepository.GetContentSourceAsync(id, preview)); scope.Complete(); - return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);; } - public async Task SeedAsync(IReadOnlyCollection contentTypeKeys) + public async Task SeedAsync(CancellationToken cancellationToken) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - IEnumerable contentCacheNodes = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys); - foreach (ContentCacheNode contentCacheNode in contentCacheNodes) + + foreach (Guid key in SeedKeys) { - if (contentCacheNode.IsDraft) + if(cancellationToken.IsCancellationRequested) { - continue; + break; } - // TODO: Make these expiration dates configurable. - // Never expire seeded values, we cannot do TimeSpan.MaxValue sadly, so best we can do is a year. - var entryOptions = new HybridCacheEntryOptions - { - Expiration = TimeSpan.FromDays(365), - LocalCacheExpiration = TimeSpan.FromDays(365), - }; + var cacheKey = GetCacheKey(key, false); - await _hybridCache.SetAsync( - GetCacheKey(contentCacheNode.Key, false), - contentCacheNode, - entryOptions); + // We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed. + ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync( + cacheKey, + async cancel => + { + ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false); + + // We don't want to seed drafts + if (cacheNode is null || cacheNode.IsDraft) + { + return null; + } + + return cacheNode; + }, + GetSeedEntryOptions()); + + // If the value is null, it's likely because + if (cachedValue is null) + { + await _hybridCache.RemoveAsync(cacheKey); + } } scope.Complete(); } + private HybridCacheEntryOptions GetSeedEntryOptions() => new() + { + Expiration = _cacheSettings.SeedCacheDuration, + LocalCacheExpiration = _cacheSettings.SeedCacheDuration + }; + public async Task HasContentByIdAsync(int id, bool preview = false) { Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); @@ -122,34 +166,67 @@ internal sealed class DocumentCacheService : IDocumentCacheService { using ICoreScope scope = _scopeProvider.CreateCoreScope(); + bool isSeeded = SeedKeys.Contains(content.Key); + // Always set draft node // We have nodes seperate in the cache, cause 99% of the time, you are only using one // and thus we won't get too much data when retrieving from the cache. - var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true); - await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)); + ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true); + await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState); + _scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed => + { + if(completed is false) + { + return; + } + + RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult(); + }, 1); if (content.PublishedState == PublishedState.Publishing) { var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false); - await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)); + await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState); + _scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed => + { + if(completed is false) + { + return; + } + + RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult(); + }, 1); } scope.Complete(); } + private async Task RefreshHybridCache(ContentCacheNode cacheNode, string cacheKey, bool isSeeded) + { + // If it's seeded we want it to stick around the cache for longer. + if (isSeeded) + { + await _hybridCache.SetAsync( + cacheKey, + cacheNode, + GetSeedEntryOptions()); + } + else + { + await _hybridCache.SetAsync(cacheKey, cacheNode); + } + } + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; - public async Task DeleteItemAsync(int id) + public async Task DeleteItemAsync(IContentBase content) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - await _databaseCacheRepository.DeleteContentItemAsync(id); - Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); - await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, true)); - await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, false)); - _idKeyMap.ClearCache(keyAttempt.Result); - _idKeyMap.ClearCache(id); + await _databaseCacheRepository.DeleteContentItemAsync(content.Id); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)); scope.Complete(); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs index 794c22b261..280e0e97f0 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs @@ -9,13 +9,13 @@ public interface IDocumentCacheService Task GetByIdAsync(int id, bool preview = false); - Task SeedAsync(IReadOnlyCollection contentTypeKeys); + Task SeedAsync(CancellationToken cancellationToken); Task HasContentByIdAsync(int id, bool preview = false); Task RefreshContentAsync(IContent content); - Task DeleteItemAsync(int id); + Task DeleteItemAsync(IContentBase content); void Rebuild(IReadOnlyCollection contentTypeKeys); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs index ad5ed2d769..bbdf166189 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs @@ -13,5 +13,7 @@ public interface IMediaCacheService Task RefreshMediaAsync(IMedia media); - Task DeleteItemAsync(int id); + Task DeleteItemAsync(IContentBase media); + + Task SeedAsync(CancellationToken cancellationToken); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 9f62072c0d..70f49f9531 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Scoping; @@ -16,6 +18,29 @@ internal class MediaCacheService : IMediaCacheService private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; private readonly IPublishedContentFactory _publishedContentFactory; private readonly ICacheNodeFactory _cacheNodeFactory; + private readonly IEnumerable _seedKeyProviders; + private readonly CacheSettings _cacheSettings; + + private HashSet? _seedKeys; + private HashSet SeedKeys + { + get + { + if (_seedKeys is not null) + { + return _seedKeys; + } + + _seedKeys = []; + + foreach (IMediaSeedKeyProvider provider in _seedKeyProviders) + { + _seedKeys.UnionWith(provider.GetSeedKeys()); + } + + return _seedKeys; + } + } public MediaCacheService( IDatabaseCacheRepository databaseCacheRepository, @@ -23,7 +48,9 @@ internal class MediaCacheService : IMediaCacheService ICoreScopeProvider scopeProvider, Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, IPublishedContentFactory publishedContentFactory, - ICacheNodeFactory cacheNodeFactory) + ICacheNodeFactory cacheNodeFactory, + IEnumerable seedKeyProviders, + IOptions cacheSettings) { _databaseCacheRepository = databaseCacheRepository; _idKeyMap = idKeyMap; @@ -31,6 +58,8 @@ internal class MediaCacheService : IMediaCacheService _hybridCache = hybridCache; _publishedContentFactory = publishedContentFactory; _cacheNodeFactory = cacheNodeFactory; + _seedKeyProviders = seedKeyProviders; + _cacheSettings = cacheSettings.Value; } public async Task GetByKeyAsync(Guid key) @@ -100,21 +129,45 @@ internal class MediaCacheService : IMediaCacheService scope.Complete(); } - public async Task DeleteItemAsync(int id) + public async Task DeleteItemAsync(IContentBase media) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - await _databaseCacheRepository.DeleteContentItemAsync(id); - Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media); - if (keyAttempt.Success) - { - await _hybridCache.RemoveAsync(keyAttempt.Result.ToString()); - } + await _databaseCacheRepository.DeleteContentItemAsync(media.Id); + await _hybridCache.RemoveAsync(media.Key.ToString()); + scope.Complete(); + } - _idKeyMap.ClearCache(keyAttempt.Result); - _idKeyMap.ClearCache(id); + public async Task SeedAsync(CancellationToken cancellationToken) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + foreach (Guid key in SeedKeys) + { + if(cancellationToken.IsCancellationRequested) + { + break; + } + + var cacheKey = GetCacheKey(key, false); + + ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync( + cacheKey, + async cancel => await _databaseCacheRepository.GetMediaSourceAsync(key), + GetSeedEntryOptions()); + + if (cachedValue is null) + { + await _hybridCache.RemoveAsync(cacheKey); + } + } scope.Complete(); } + private HybridCacheEntryOptions GetSeedEntryOptions() => new() + { + Expiration = _cacheSettings.SeedCacheDuration, LocalCacheExpiration = _cacheSettings.SeedCacheDuration, + }; + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj index 41fb4becbc..6068233712 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj +++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj @@ -25,6 +25,9 @@ <_Parameter1>DynamicProxyGenAssembly2 + + <_Parameter1>Umbraco.Tests.UnitTests + diff --git a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs index 92f65bbc39..069a0d82b2 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; @@ -17,10 +16,10 @@ public class ContentEditingBuilder IWithKeyBuilder, IWithContentTypeKeyBuilder, IWithParentKeyBuilder, - IWithTemplateKeyBuilder + IWithTemplateKeyBuilder, + IBuildContentTypes { - private IContentType _contentType; - private ContentTypeBuilder _contentTypeBuilder; + private ContentTypeEditingBuilder _contentTypeEditingBuilder; private IEnumerable _invariantProperties = []; private IEnumerable _variants = []; private Guid _contentTypeKey; @@ -84,8 +83,7 @@ public class ContentEditingBuilder return this; } - public ContentEditingBuilder AddVariant(string culture, string segment, string name, - IEnumerable properties) + public ContentEditingBuilder AddVariant(string culture, string segment, string name, IEnumerable properties) { var variant = new VariantModel { Culture = culture, Segment = segment, Name = name, Properties = properties }; _variants = _variants.Concat(new[] { variant }); @@ -104,13 +102,6 @@ public class ContentEditingBuilder return this; } - public ContentEditingBuilder WithContentType(IContentType contentType) - { - _contentTypeBuilder = null; - _contentType = contentType; - return this; - } - public override ContentCreateModel Build() { var key = _key ?? Guid.NewGuid(); @@ -120,15 +111,7 @@ public class ContentEditingBuilder var invariantProperties = _invariantProperties; var variants = _variants; - if (_contentTypeBuilder is null && _contentType is null) - { - throw new InvalidOperationException( - "A content item cannot be constructed without providing a content type. Use AddContentType() or WithContentType()."); - } - - var contentType = _contentType ?? _contentTypeBuilder.Build(); var content = new ContentCreateModel(); - content.InvariantName = invariantName; if (parentKey is not null) { @@ -140,7 +123,7 @@ public class ContentEditingBuilder content.TemplateKey = templateKey; } - content.ContentTypeKey = contentType.Key; + content.ContentTypeKey = _contentTypeKey; content.Key = key; content.InvariantProperties = invariantProperties; content.Variants = variants; @@ -148,25 +131,39 @@ public class ContentEditingBuilder return content; } - public static ContentCreateModel CreateBasicContent(IContentType contentType, Guid? key) => + public static ContentCreateModel CreateBasicContent(Guid contentTypeKey, Guid? key) => new ContentEditingBuilder() .WithKey(key) - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName("Home") .Build(); - public static ContentCreateModel CreateSimpleContent(IContentType contentType) => + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey) => new ContentEditingBuilder() - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName("Home") .WithInvariantProperty("title", "Welcome to our Home page") .Build(); - public static ContentCreateModel CreateSimpleContent(IContentType contentType, string name, Guid? parentKey) => + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name, Guid? parentKey) => new ContentEditingBuilder() - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName(name) .WithParentKey(parentKey) .WithInvariantProperty("title", "Welcome to our Home page") .Build(); + + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name) => + new ContentEditingBuilder() + .WithContentTypeKey(contentTypeKey) + .WithInvariantName(name) + .WithInvariantProperty("title", "Welcome to our Home page") + .Build(); + + public static ContentCreateModel CreateContentWithTwoVariantProperties(Guid contentTypeKey, string firstCulture, string secondCulture, string propertyAlias, string propertyName) => + new ContentEditingBuilder() + .WithContentTypeKey(contentTypeKey) + .AddVariant(firstCulture, null, firstCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } }) + .AddVariant(secondCulture, null, secondCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } }) + .Build(); } diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs new file mode 100644 index 0000000000..bcbcbcada0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs @@ -0,0 +1,240 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class ContentTypeEditingBuilder + : ContentTypeBaseBuilder, + IBuildPropertyTypes +{ + private Guid? _key; + private Guid? _containerKey; + private ContentTypeCleanup _cleanup = new(); + private IEnumerable _allowedTemplateKeys; + private Guid? _defaultTemplateKey; + private bool? _allowAtRoot; + private bool? _isElement; + private bool? _variesByCulture; + private bool? _variesBySegment; + private readonly List _propertyTypeBuilders = []; + private readonly List> _propertyTypeContainerBuilders = []; + private readonly List _allowedContentTypeBuilders = []; + + public ContentTypeEditingBuilder() + : base(null) + { + } + + public ContentTypeEditingBuilder(ContentEditingBuilder parentBuilder) + : base(parentBuilder) + { + } + + public ContentTypeEditingBuilder WithDefaultTemplateKey(Guid templateKey) + { + _defaultTemplateKey = templateKey; + return this; + } + + public ContentTypeEditingBuilder WithIsElement(bool isElement) + { + _isElement = isElement; + return this; + } + + public PropertyTypeContainerBuilder AddPropertyGroup() + { + var builder = new PropertyTypeContainerBuilder(this); + _propertyTypeContainerBuilders.Add(builder); + return builder; + } + + public PropertyTypeEditingBuilder AddPropertyType() + { + var builder = new PropertyTypeEditingBuilder(this); + _propertyTypeBuilders.Add(builder); + return builder; + } + + + public ContentTypeSortBuilder AddAllowedContentType() + { + var builder = new ContentTypeSortBuilder(this); + _allowedContentTypeBuilders.Add(builder); + return builder; + } + + public ContentTypeEditingBuilder AddAllowedTemplateKeys(IEnumerable templateKeys) + { + _allowedTemplateKeys = templateKeys; + return this; + } + + public ContentTypeEditingBuilder WithAllowAtRoot(bool allowAtRoot) + { + _allowAtRoot = allowAtRoot; + return this; + } + + public ContentTypeEditingBuilder WithVariesByCulture(bool variesByCulture) + { + _variesByCulture = variesByCulture; + return this; + } + + public ContentTypeEditingBuilder WithVariesBySegment(bool variesBySegment) + { + _variesBySegment = variesBySegment; + return this; + } + + public override ContentTypeCreateModel Build() + { + ContentTypeCreateModel contentType = new ContentTypeCreateModel(); + contentType.Name = GetName(); + contentType.Alias = GetAlias(); + contentType.Key = GetKey(); + contentType.ContainerKey = _containerKey; + contentType.Cleanup = _cleanup; + contentType.AllowedTemplateKeys = _allowedTemplateKeys ?? Array.Empty(); + contentType.DefaultTemplateKey = _defaultTemplateKey; + contentType.IsElement = _isElement ?? false; + contentType.VariesByCulture = _variesByCulture ?? false; + contentType.VariesBySegment = _variesBySegment ?? false; + contentType.AllowedAsRoot = _allowAtRoot ?? false; + contentType.Properties = _propertyTypeBuilders.Select(x => x.Build()); + contentType.Containers = _propertyTypeContainerBuilders.Select(x => x.Build()); + contentType.AllowedContentTypes = _allowedContentTypeBuilders.Select(x => x.Build()); + + return contentType; + } + + public static ContentTypeCreateModel CreateBasicContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null) + { + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithParentContentType(parent) + .Build(); + } + + public static ContentTypeCreateModel CreateSimpleContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null, string propertyGroupName = "Content", Guid? defaultTemplateKey = null) + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithAllowAtRoot(true) + .WithParentContentType(parent) + .AddPropertyGroup() + .WithKey(containerKey) + .WithName(propertyGroupName) + .Done() + .AddPropertyType() + .WithAlias("title") + .WithDataTypeKey(Constants.DataTypes.Guids.TextareaGuid) + .WithName("Title") + .WithContainerKey(containerKey) + .Done() + .WithDefaultTemplateKey(defaultTemplateKey ?? Guid.Empty) + .AddAllowedTemplateKeys([defaultTemplateKey ?? Guid.Empty]) + .Build(); + } + + public static ContentTypeCreateModel CreateTextPageContentType(string alias = "textPage", string name = "Text Page", Guid defaultTemplateKey = default) + { + var containerKeyOne = Guid.NewGuid(); + var containerKeyTwo = Guid.NewGuid(); + + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithAllowAtRoot(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKeyOne) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithContainerKey(containerKeyOne) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid) + .WithAlias("bodyText") + .WithName("Body text") + .WithContainerKey(containerKeyOne) + .WithSortOrder(2) + .Done() + .AddPropertyGroup() + .WithName("Meta") + .WithSortOrder(2) + .WithKey(containerKeyTwo) + .Done() + .AddPropertyType() + .WithAlias("keywords") + .WithName("Keywords") + .WithContainerKey(containerKeyTwo) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithAlias("description") + .WithName("Description") + .WithContainerKey(containerKeyTwo) + .WithSortOrder(2) + .Done() + .AddAllowedTemplateKeys([defaultTemplateKey]) + .WithDefaultTemplateKey(defaultTemplateKey) + .Build(); + } + + public static ContentTypeCreateModel CreateElementType(string alias = "textElement", string name = "Text Element") + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithIsElement(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKey) + .Done() + .AddPropertyType() + .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid) + .WithAlias("bodyText") + .WithName("Body text") + .WithContainerKey(containerKey) + .Done() + .Build(); + } + + public static ContentTypeCreateModel CreateContentTypeWithDataTypeKey(Guid dataTypeKey, string alias = "textElement", string name = "Text Element" ) + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithIsElement(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKey) + .Done() + .AddPropertyType() + .WithDataTypeKey(dataTypeKey) + .WithAlias("dataType") + .WithName("Data Type") + .WithContainerKey(containerKey) + .Done() + .Build(); + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs index 7a4deca5f7..a63205d4d2 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs @@ -28,6 +28,11 @@ public class ContentTypeSortBuilder { } + public ContentTypeSortBuilder(ContentTypeEditingBuilder parentBuilder) + : base(null) + { + } + string IWithAliasBuilder.Alias { get => _alias; diff --git a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index 1a4660ed64..f4cc7db311 100644 --- a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -80,6 +80,13 @@ public static class BuilderExtensions return builder; } + public static T WithDataTypeKey(this T builder, Guid key) + where T : IWithDataTypeKeyBuilder + { + builder.DataTypeKey = key; + return builder; + } + public static T WithParentId(this T builder, int parentId) where T : IWithParentIdBuilder { diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs new file mode 100644 index 0000000000..cc184eea48 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWIthContainerKeyBuilder +{ + Guid? ContainerKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs new file mode 100644 index 0000000000..ff188b8017 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithDataTypeKeyBuilder +{ + Guid? DataTypeKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs new file mode 100644 index 0000000000..e86c18a1dc --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithLabelOnTop +{ + public bool? LabelOnTop { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs new file mode 100644 index 0000000000..04bea1ecb9 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithMandatoryBuilder +{ + bool? Mandatory { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs new file mode 100644 index 0000000000..0f5c510dc0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithMandatoryMessageBuilder +{ + string MandatoryMessage { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs new file mode 100644 index 0000000000..b6cdc81651 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithRegularExpressionBuilder +{ + string RegularExpression { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs new file mode 100644 index 0000000000..6d2e0eef66 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithRegularExpressionMessage +{ + string RegularExpressionMessage { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs new file mode 100644 index 0000000000..8d25ce8a55 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithTypeBuilder +{ + public string Type { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs new file mode 100644 index 0000000000..fe347f30c0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithVariesByCultureBuilder +{ + bool VariesByCulture { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs new file mode 100644 index 0000000000..0d9ea748e1 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithVariesBySegmentBuilder +{ + bool VariesBySegment { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs new file mode 100644 index 0000000000..4917b2861b --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeAppearanceBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithLabelOnTop +{ + private bool? _labelOnTop; + + public PropertyTypeAppearanceBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + } + + bool? IWithLabelOnTop.LabelOnTop + { + get => _labelOnTop; + set => _labelOnTop = value; + } + + public override PropertyTypeAppearance Build() => new() { LabelOnTop = _labelOnTop ?? false }; +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs new file mode 100644 index 0000000000..b5af22eb50 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs @@ -0,0 +1,66 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeContainerBuilder(TParent parentBuilder) + : ChildBuilderBase(parentBuilder), + IBuildPropertyTypes, IWithKeyBuilder, IWithParentKeyBuilder, IWithNameBuilder, IWithTypeBuilder, + IWithSortOrderBuilder +{ + private Guid? _key; + private Guid? _parentKey; + private string _name; + private string _type; + private int? _sortOrder; + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + Guid? IWithParentKeyBuilder.ParentKey + { + get => _parentKey; + set => _parentKey = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + string IWithTypeBuilder.Type + { + get => _type; + set => _type = value; + } + + int? IWithSortOrderBuilder.SortOrder + { + get => _sortOrder; + set => _sortOrder = value; + } + + public override ContentTypePropertyContainerModel Build() + { + var key = _key ?? Guid.NewGuid(); + var parentKey = _parentKey; + var name = _name ?? "Container"; + var type = _type ?? "Group"; + var sortOrder = _sortOrder ?? 0; + + + return new ContentTypePropertyContainerModel + { + Key = key, + ParentKey = parentKey, + Name = name, + Type = type, + SortOrder = sortOrder, + }; + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs new file mode 100644 index 0000000000..301d77f37b --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs @@ -0,0 +1,176 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeEditingBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithKeyBuilder, + IWIthContainerKeyBuilder, + IWithSortOrderBuilder, IWithAliasBuilder, IWithNameBuilder, IWithDescriptionBuilder, IWithDataTypeKeyBuilder, + IWithVariesByCultureBuilder, IWithVariesBySegmentBuilder +{ + private Guid? _key; + private Guid? _containerKey; + private int? _sortOrder; + private string _alias; + private string? _name; + private string? _description; + private Guid? _dataTypeKey; + private bool _variesByCulture; + private bool _variesBySegment; + private PropertyTypeValidationEditingBuilder _validationBuilder; + private PropertyTypeAppearanceBuilder _appearanceBuilder; + + public PropertyTypeEditingBuilder(ContentTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + _validationBuilder = new PropertyTypeValidationEditingBuilder(this); + _appearanceBuilder = new PropertyTypeAppearanceBuilder(this); + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + Guid? IWIthContainerKeyBuilder.ContainerKey + { + get => _containerKey; + set => _containerKey = value; + } + + int? IWithSortOrderBuilder.SortOrder + { + get => _sortOrder; + set => _sortOrder = value; + } + + string IWithAliasBuilder.Alias + { + get => _alias; + set => _alias = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + string IWithDescriptionBuilder.Description + { + get => _description; + set => _description = value; + } + + Guid? IWithDataTypeKeyBuilder.DataTypeKey + { + get => _dataTypeKey; + set => _dataTypeKey = value; + } + + bool IWithVariesByCultureBuilder.VariesByCulture + { + get => _variesByCulture; + set => _variesByCulture = value; + } + + bool IWithVariesBySegmentBuilder.VariesBySegment + { + get => _variesBySegment; + set => _variesBySegment = value; + } + + public PropertyTypeValidationEditingBuilder AddValidation() + { + var builder = new PropertyTypeValidationEditingBuilder(this); + _validationBuilder = builder; + return builder; + } + + public PropertyTypeAppearanceBuilder AddAppearance() + { + var builder = new PropertyTypeAppearanceBuilder(this); + _appearanceBuilder = builder; + return builder; + } + + public PropertyTypeEditingBuilder WithContainerKey(Guid? containerKey) + { + _containerKey = containerKey; + return this; + } + + public PropertyTypeEditingBuilder WithSortOrder(int sortOrder) + { + _sortOrder = sortOrder; + return this; + } + + public PropertyTypeEditingBuilder WithAlias(string alias) + { + _alias = alias; + return this; + } + + public PropertyTypeEditingBuilder WithName(string name) + { + _name = name; + return this; + } + + public PropertyTypeEditingBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public PropertyTypeEditingBuilder WithDataTypeKey(Guid dataTypeKey) + { + _dataTypeKey = dataTypeKey; + return this; + } + + public PropertyTypeEditingBuilder WithVariesByCulture(bool variesByCulture) + { + _variesByCulture = variesByCulture; + return this; + } + + public PropertyTypeEditingBuilder WithVariesBySegment(bool variesBySegment) + { + _variesBySegment = variesBySegment; + return this; + } + + public override ContentTypePropertyTypeModel Build() + { + var key = _key ?? Guid.NewGuid(); + var containerKey = _containerKey; + var sortOrder = _sortOrder ?? 0; + var alias = _alias ?? "title"; + var name = _name ?? "Title"; + var description = _description; + var dataTypeKey = _dataTypeKey ?? Constants.DataTypes.Guids.TextareaGuid; + var variesByCulture = _variesByCulture; + var variesBySegment = _variesBySegment; + var validation = _validationBuilder.Build(); + var appearance = _appearanceBuilder.Build(); + + return new ContentTypePropertyTypeModel + { + Key = key, + ContainerKey = containerKey, + SortOrder = sortOrder, + Alias = alias, + Name = name, + Description = description, + DataTypeKey = dataTypeKey, + VariesByCulture = variesByCulture, + VariesBySegment = variesBySegment, + Validation = validation, + Appearance = appearance, + }; + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs new file mode 100644 index 0000000000..781463c760 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeValidationEditingBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithMandatoryBuilder, + IWithMandatoryMessageBuilder, IWithRegularExpressionBuilder, IWithRegularExpressionMessage +{ + private bool? _mandatory; + private string? _mandatoryMessage; + private string? _regularExpression; + private string? _regularExpressionMessage; + + public PropertyTypeValidationEditingBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + } + + bool? IWithMandatoryBuilder.Mandatory + { + get => _mandatory; + set => _mandatory = value; + } + + string? IWithMandatoryMessageBuilder.MandatoryMessage + { + get => _mandatoryMessage; + set => _mandatoryMessage = value; + } + + string? IWithRegularExpressionBuilder.RegularExpression + { + get => _regularExpression; + set => _regularExpression = value; + } + + string? IWithRegularExpressionMessage.RegularExpressionMessage + { + get => _regularExpressionMessage; + set => _regularExpressionMessage = value; + } + + public override PropertyTypeValidation Build() + { + var validation = new PropertyTypeValidation + { + Mandatory = _mandatory ?? false, + MandatoryMessage = _mandatoryMessage ?? null, + RegularExpression = _regularExpression ?? null, + RegularExpressionMessage = _regularExpressionMessage ?? null, + }; + + return validation; + } +} diff --git a/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs new file mode 100644 index 0000000000..d2d083cde7 --- /dev/null +++ b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs @@ -0,0 +1,83 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Common.TestHelpers; + +public class ContentTypeUpdateHelper +{ + public ContentTypeUpdateModel CreateContentTypeUpdateModel(IContentType contentType) + { + var updateModel = new ContentTypeUpdateModel(); + var model = MapBaseProperties(contentType, updateModel); + return model; + } + + private T MapBaseProperties(IContentType contentType, T model) where T : ContentTypeModelBase + { + model.Alias = contentType.Alias; + model.Name = contentType.Name; + model.Description = contentType.Description; + model.Icon = contentType.Icon; + model.AllowedAsRoot = contentType.AllowedAsRoot; + model.VariesByCulture = contentType.VariesByCulture(); + model.VariesBySegment = contentType.VariesBySegment(); + model.IsElement = contentType.IsElement; + model.ListView = contentType.ListView; + model.Cleanup = new ContentTypeCleanup() + { + PreventCleanup = contentType.HistoryCleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = contentType.HistoryCleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = contentType.HistoryCleanup.KeepLatestVersionPerDayForDays + }; + + model.AllowedTemplateKeys = contentType.AllowedTemplates.Select(x => x.Key); + model.DefaultTemplateKey = contentType.DefaultTemplate?.Key; + + var tempContainerList = model.Containers.ToList(); + + foreach (var container in contentType.PropertyGroups) + { + var containerModel = new ContentTypePropertyContainerModel() + { + Key = container.Key, + Name = container.Name, + SortOrder = container.SortOrder, + Type = container.Type.ToString() + }; + tempContainerList.Add(containerModel); + } + + model.Containers = tempContainerList.AsEnumerable(); + + var tempPropertyList = model.Properties.ToList(); + + foreach (var propertyType in contentType.PropertyTypes) + { + var propertyModel = new ContentTypePropertyTypeModel + { + Key = propertyType.Key, + ContainerKey = contentType.PropertyGroups.Single(x => x.PropertyTypes.Contains(propertyType)).Key, + SortOrder = propertyType.SortOrder, + Alias = propertyType.Alias, + Name = propertyType.Name, + Description = propertyType.Description, + DataTypeKey = propertyType.DataTypeKey, + VariesByCulture = propertyType.VariesByCulture(), + VariesBySegment = propertyType.VariesBySegment(), + Validation = new PropertyTypeValidation() + { + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.ValidationRegExp, + RegularExpression = propertyType.ValidationRegExp, + RegularExpressionMessage = propertyType.ValidationRegExpMessage, + }, + Appearance = new PropertyTypeAppearance() { LabelOnTop = propertyType.LabelOnTop, } + }; + tempPropertyList.Add(propertyModel); + } + + model.Properties = tempPropertyList.AsEnumerable(); + return model; + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs index 10dd0cb467..e1f047dd90 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs @@ -6,28 +6,31 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.ContentTypeEditing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; namespace Umbraco.Cms.Tests.Integration.Testing; public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrationTest { - protected IContentTypeService ContentTypeService => GetRequiredService(); + protected IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); protected ITemplateService TemplateService => GetRequiredService(); - private ContentEditingService ContentEditingService => - (ContentEditingService)GetRequiredService(); + private IContentEditingService ContentEditingService => (IContentEditingService)GetRequiredService(); - private ContentPublishingService ContentPublishingService => - (ContentPublishingService)GetRequiredService(); + private IContentPublishingService ContentPublishingService => (IContentPublishingService)GetRequiredService(); + protected int TemplateId { get; private set; } + + protected ContentCreateModel Subpage1 { get; private set; } protected ContentCreateModel Subpage2 { get; private set; } - protected ContentCreateModel Subpage3 { get; private set; } - protected ContentCreateModel Subpage { get; private set; } + protected ContentCreateModel PublishedTextPage { get; private set; } protected ContentCreateModel Textpage { get; private set; } @@ -37,13 +40,17 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat protected int TextpageId { get; private set; } - protected int SubpageId { get; private set; } + protected int PublishedTextPageId { get; private set; } + + protected int Subpage1Id { get; private set; } protected int Subpage2Id { get; private set; } - protected int Subpage3Id { get; private set; } + protected ContentTypeCreateModel ContentTypeCreateModel { get; private set; } - protected ContentType ContentType { get; private set; } + protected ContentTypeUpdateModel ContentTypeUpdateModel { get; private set; } + + protected IContentType ContentType { get; private set; } [SetUp] public new void Setup() => CreateTestData(); @@ -53,19 +60,24 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - + TemplateId = template.Id; // Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type) - ContentType = - ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); - ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); - ContentType.AllowedAsRoot = true; - ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; - var contentTypeResult = await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); - Assert.IsTrue(contentTypeResult.Success); + ContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateKey: template.Key); + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(contentTypeAttempt.Success); + + var contentTypeResult = contentTypeAttempt.Result; + ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper(); + ContentTypeUpdateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(contentTypeResult); ContentTypeUpdateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(contentTypeResult.Key, 0, ContentTypeCreateModel.Alias), + }; + var updatedContentTypeResult = await ContentTypeEditingService.UpdateAsync(contentTypeResult, ContentTypeUpdateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updatedContentTypeResult.Success); + ContentType = updatedContentTypeResult.Result; // Create and Save Content "Homepage" based on "umbTextpage" -> 1053 - Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType); - Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key); var createContentResultTextPage = await ContentEditingService.CreateAsync(Textpage, Constants.Security.SuperUserKey); Assert.IsTrue(createContentResultTextPage.Success); @@ -87,24 +99,38 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat }; // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 - Subpage = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Key); - var createContentResultSubPage = await ContentEditingService.CreateAsync(Subpage, Constants.Security.SuperUserKey); - Assert.IsTrue(createContentResultSubPage.Success); + PublishedTextPage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Published Page"); + var createContentResultPublishPage = await ContentEditingService.CreateAsync(PublishedTextPage, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultPublishPage.Success); - if (!Subpage.Key.HasValue) + if (!PublishedTextPage.Key.HasValue) { throw new InvalidOperationException("The content page key is null."); } - if (createContentResultSubPage.Result.Content != null) + if (createContentResultPublishPage.Result.Content != null) { - SubpageId = createContentResultSubPage.Result.Content.Id; + PublishedTextPageId = createContentResultPublishPage.Result.Content.Id; } - await ContentPublishingService.PublishAsync(Subpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var publishResult = await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 - Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Key); + Subpage1 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 1", Textpage.Key); + var createContentResultSubPage1 = await ContentEditingService.CreateAsync(Subpage1, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultSubPage1.Success); + if (!Subpage1.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultSubPage1.Result.Content != null) + { + Subpage1Id = createContentResultSubPage1.Result.Content.Id; + } + + Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 2", Textpage.Key); var createContentResultSubPage2 = await ContentEditingService.CreateAsync(Subpage2, Constants.Security.SuperUserKey); Assert.IsTrue(createContentResultSubPage2.Success); if (!Subpage2.Key.HasValue) @@ -116,18 +142,5 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat { Subpage2Id = createContentResultSubPage2.Result.Content.Id; } - - Subpage3 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Key); - var createContentResultSubPage3 = await ContentEditingService.CreateAsync(Subpage3, Constants.Security.SuperUserKey); - Assert.IsTrue(createContentResultSubPage3.Success); - if (!Subpage3.Key.HasValue) - { - throw new InvalidOperationException("The content page key is null."); - } - - if (createContentResultSubPage3.Result.Content != null) - { - Subpage3Id = createContentResultSubPage3.Result.Content.Id; - } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs new file mode 100644 index 0000000000..7c4b62f2af --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class PublishedContentTypeCacheTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService(); + private IContentTypeService ContentTypeService => GetRequiredService(); + + [Test] + public async Task Can_Get_Published_DocumentType_By_Key() + { + // Act + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + + // Assert + Assert.IsNotNull(contentType); + } + + [Test] + public async Task Can_Get_Updated_Published_DocumentType_By_Key() + { + // Arrange + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); + Assert.IsNotNull(contentType); + Assert.AreEqual(1, ContentType.PropertyTypes.Count()); + // Update the content type + ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper(); + var updateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(ContentType); + updateModel.Properties = new List(); + await ContentTypeEditingService.UpdateAsync(ContentType, updateModel, Constants.Security.SuperUserKey); + + // Act + var updatedContentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + + // Assert + Assert.IsNotNull(updatedContentType); + Assert.AreEqual(0, updatedContentType.PropertyTypes.Count()); + } + + [Test] + public async Task Published_DocumentType_Gets_Deleted() + { + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + Assert.IsNotNull(contentType); + + await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey); + Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs index f0c70c5911..57503c71f2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -16,16 +16,16 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); - private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService(); + private IContentTypeService ContentTypeService => GetRequiredService(); [Test] public async Task Can_Get_Draft_Content_By_Id() { //Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); ContentType.RemovePropertyType("title"); - ContentTypeService.Save(ContentType); + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); // Assert var newTextPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); @@ -36,10 +36,10 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo public async Task Can_Get_Draft_Content_By_Key() { // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); ContentType.RemovePropertyType("title"); - ContentTypeService.Save(ContentType); + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); //Assert var newTextPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); Assert.IsNull(newTextPage.Value("title")); @@ -57,26 +57,4 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo var textpageAgain = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true); Assert.IsNull(textpageAgain); } - - - // TODO: Copy this into PublishedContentTypeCache - [Test] - public async Task Can_Get_Published_DocumentType_By_Key() - { - var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - var contentTypeAgain = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - } - - [Test] - public async Task Published_DocumentType_Gets_Deleted() - { - var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - - await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey); - // PublishedContentTypeCache just explodes if it doesn't exist - Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey)); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs index ac7c55604f..5fc467f2f6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -1,4 +1,5 @@ -using Moq; +using Microsoft.Extensions.Options; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -7,9 +8,11 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; using Umbraco.Cms.Infrastructure.HybridCache.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -29,6 +32,8 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent private IContentPublishingService ContentPublishingService => GetRequiredService(); + private CacheSettings _cacheSettings; + [SetUp] public void SetUp() { @@ -44,33 +49,48 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent false, new Dictionary(), null); - _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync( - new ContentCacheNode() - { - ContentTypeId = Textpage.ContentTypeId, - CreatorId = Textpage.CreatorId, - CreateDate = Textpage.CreateDate, - Id = Textpage.Id, - Key = Textpage.Key, - SortOrder = 0, - Data = contentData, - IsDraft = true, - }); + + + var draftTestCacheNode = new ContentCacheNode() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = true, + }; + + var publishedTestCacheNode = new ContentCacheNode() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = false, + }; + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), true)) + .ReturnsAsync(draftTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false)) + .ReturnsAsync(publishedTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), true)) + .ReturnsAsync(draftTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false)) + .ReturnsAsync(publishedTestCacheNode); _mockedNucacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny>())).Returns( new List() { - new() - { - ContentTypeId = Textpage.ContentTypeId, - CreatorId = Textpage.CreatorId, - CreateDate = Textpage.CreateDate, - Id = Textpage.Id, - Key = Textpage.Key, - SortOrder = 0, - Data = contentData, - IsDraft = false, - }, + draftTestCacheNode, }); _mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny())); @@ -81,11 +101,30 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent GetRequiredService(), GetRequiredService(), GetRequiredService(), - GetRequiredService()); + GetRequiredService(), + GetSeedProviders(), + Options.Create(new CacheSettings())); _mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService()); } + // We want to be able to alter the settings for the providers AFTER the test has started + // So we'll manually create them with a magic options mock. + private IEnumerable GetSeedProviders() + { + _cacheSettings = new CacheSettings(); + _cacheSettings.DocumentBreadthFirstSeedCount = 0; + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(() => _cacheSettings); + + return new List + { + new ContentTypeSeedKeyProvider(GetRequiredService(), GetRequiredService(), mock.Object), + new DocumentBreadthFirstKeyProvider(GetRequiredService(), mock.Object), + }; + } + [Test] public async Task Content_Is_Cached_By_Key() { @@ -95,7 +134,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Key, true); AssertTextPage(textPage); AssertTextPage(textPage2); - _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); } [Test] @@ -121,9 +160,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); Assert.IsTrue(publishResult.Success); Textpage.Published = true; - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id); AssertTextPage(textPage); @@ -141,9 +181,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); Assert.IsTrue(publishResult.Success); Textpage.Published = true; - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key); AssertTextPage(textPage); @@ -151,12 +192,13 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent } [Test] - public async Task Content_Is_Not_Seeded_If_Unpublished_By_Id() + public async Task Content_Is_Not_Seeded_If_Unpblished_By_Id() { - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true); AssertTextPage(textPage); @@ -166,13 +208,14 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent [Test] public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key() { - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true); AssertTextPage(textPage); - _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); } private void AssertTextPage(IPublishedContent textPage) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs index 0cfc342917..68eddf35df 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -25,23 +26,25 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest private ITemplateService TemplateService => GetRequiredService(); - private IContentTypeService ContentTypeService => GetRequiredService(); - private IContentEditingService ContentEditingService => GetRequiredService(); - private IContentPublishingService ContentPublishingService => GetRequiredService(); + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + private IContentPublishingService ContentPublishingService => GetRequiredService(); [Test] public async Task Can_Get_Value_From_ContentPicker() { + // Arrange var template = TemplateBuilder.CreateTextPageTemplate(); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - var textPage = await CreateTextPageDocument(template.Id); - var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + var textPage = await CreateTextPageDocument(template.Key); + var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key); + // Act var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + // Assert IPublishedContent contentPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); Assert.AreEqual(textPage.Key, contentPickerValue.Key); Assert.AreEqual(textPage.Id, contentPickerValue.Id); @@ -52,10 +55,11 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest [Test] public async Task Can_Get_Value_From_Updated_ContentPicker() { + // Arrange var template = TemplateBuilder.CreateTextPageTemplate(); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - var textPage = await CreateTextPageDocument(template.Id); - var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + var textPage = await CreateTextPageDocument(template.Key); + var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key); // Get for caching var notUpdatedContent = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); @@ -88,46 +92,42 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest Assert.IsTrue(publishResult); + // Act var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + + // Assert IPublishedContent updatedPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); - - Assert.AreEqual(textPage.Key, updatedPickerValue.Key); Assert.AreEqual(textPage.Id, updatedPickerValue.Id); Assert.AreEqual(textPage.Name, updatedPickerValue.Name); Assert.AreEqual("Updated title", updatedPickerValue.Properties.First(x => x.Alias == "title").GetValue()); } - private async Task CreateContentPickerDocument(int templateId, Guid textPageKey) + private async Task CreateContentPickerDocument(Guid templateKey, Guid textPageKey) { - var builder = new ContentTypeBuilder(); - var pickerContentType = (ContentType)builder + var builder = new ContentTypeEditingBuilder(); + var pickerContentType = builder .WithAlias("test") .WithName("TestName") - .AddAllowedTemplate() - .WithId(templateId) - .Done() + .WithAllowAtRoot(true) + .AddAllowedTemplateKeys([templateKey]) .AddPropertyGroup() - .WithName("Content") - .WithSupportsPublishing(true) + .WithName("Content") + .Done() .AddPropertyType() - .WithAlias("contentPicker") - .WithName("Content Picker") - .WithDataTypeId(1046) - .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.ContentPicker) - .WithValueStorageType(ValueStorageType.Integer) - .WithSortOrder(16) - .Done() - .Done() + .WithAlias("contentPicker") + .WithName("Content Picker") + .WithDataTypeKey(Constants.DataTypes.Guids.ContentPickerGuid) + .WithSortOrder(16) + .Done() .Build(); - pickerContentType.AllowedAsRoot = true; - ContentTypeService.Save(pickerContentType); + await ContentTypeEditingService.CreateAsync(pickerContentType, Constants.Security.SuperUserKey); var createOtherModel = new ContentCreateModel { - ContentTypeKey = pickerContentType.Key, + ContentTypeKey = pickerContentType.Key.Value, ParentKey = Constants.System.RootKey, InvariantName = "Test Create", InvariantProperties = new[] { new PropertyValueModel { Alias = "contentPicker", Value = textPageKey }, }, @@ -149,15 +149,14 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest return result.Result.Content; } - private async Task CreateTextPageDocument(int templateId) + private async Task CreateTextPageDocument(Guid templateKey) { - var textContentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: templateId); - textContentType.AllowedAsRoot = true; - ContentTypeService.Save(textContentType); + var textContentType = ContentTypeEditingBuilder.CreateTextPageContentType(defaultTemplateKey: templateKey); + await ContentTypeEditingService.CreateAsync(textContentType, Constants.Security.SuperUserKey); var createModel = new ContentCreateModel { - ContentTypeKey = textContentType.Key, + ContentTypeKey = textContentType.Key.Value, ParentKey = Constants.System.RootKey, InvariantName = "Root Create", InvariantProperties = new[] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs new file mode 100644 index 0000000000..d7d04b64fb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheTemplateTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + [Test] + public async Task Can_Get_Document_After_Removing_Template() + { + // Arrange + var textPageBefore = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + Assert.AreEqual(textPageBefore.TemplateId, TemplateId); + var updateModel = new ContentUpdateModel(); + { + updateModel.TemplateKey = null; + updateModel.InvariantName = textPageBefore.Name; + } + + // Act + var updateContentResult = await ContentEditingService.UpdateAsync(textPageBefore.Key, updateModel, Constants.Security.SuperUserKey); + + // Assert + Assert.AreEqual(updateContentResult.Status, ContentEditingOperationStatus.Success); + var textPageAfter = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + // Should this not be null? + Assert.AreEqual(textPageAfter.TemplateId, null); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs index 7d8d4123e1..bf2bfaddb4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -25,16 +25,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing private const string NewName = "New Name"; private const string NewTitle = "New Title"; - - // Create CRUD Tests for Content, Also cultures. - [Test] public async Task Can_Get_Draft_Content_By_Id() { - //Act + // Act var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - //Assert + // Assert AssertTextPage(textPage); } @@ -51,54 +48,42 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing [Test] public async Task Can_Get_Published_Content_By_Id() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); } [Test] public async Task Can_Get_Published_Content_By_Key() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); } [Test] public async Task Can_Get_Draft_Of_Published_Content_By_Id() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); Assert.IsFalse(textPage.IsPublished()); } [Test] public async Task Can_Get_Draft_Of_Published_Content_By_Key() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); Assert.IsFalse(textPage.IsPublished()); } @@ -151,19 +136,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantName = NewName; + PublishedTextPage.InvariantName = NewName; ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.AreEqual(result, NewName.Equals(textPage.Name)); @@ -176,22 +160,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - - Textpage.InvariantName = NewName; - + PublishedTextPage.InvariantName = NewName; ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); // Assert Assert.AreEqual(result, NewName.Equals(textPage.Name)); @@ -227,11 +207,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Published_Content_Property_By_Id() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -241,11 +220,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Published_Content_Property_By_Key() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -255,11 +233,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -269,11 +246,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -284,7 +260,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing { // Arrange Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = Textpage.InvariantName, @@ -292,7 +267,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing Variants = Textpage.Variants, TemplateKey = Textpage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act @@ -307,7 +281,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing { // Arrange Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = Textpage.InvariantName, @@ -315,7 +288,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing Variants = Textpage.Variants, TemplateKey = Textpage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act @@ -329,21 +301,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Published_Content_Property_By_Id() { // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(NewTitle, textPage.Value("title")); @@ -353,21 +323,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Published_Content_Property_By_Key() { // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); // Assert Assert.AreEqual(NewTitle, textPage.Value("title")); @@ -379,21 +347,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.AreEqual(titleName, textPage.Value("title")); @@ -405,21 +370,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleName, textPage.Value("title")); @@ -429,12 +391,14 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Content_By_Id() { // Arrange - var content = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + var content = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); Assert.IsNotNull(content); - await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + var textPagePublishedContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, false); + + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); // Assert Assert.IsNull(textPage); @@ -444,11 +408,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Content_By_Key() { // Arrange - await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); - var result = await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true); + var hasContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); + Assert.IsNotNull(hasContent); + await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true); // Assert Assert.IsNull(textPage); @@ -460,11 +426,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.IsNull(textPage); @@ -476,11 +441,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); // Assert Assert.IsNull(textPage); @@ -499,6 +463,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing AssertProperties(Textpage.InvariantProperties, textPage.Properties); } + private void AssertPublishedTextPage(IPublishedContent textPage) + { + Assert.Multiple(() => + { + Assert.IsNotNull(textPage); + Assert.AreEqual(PublishedTextPage.Key, textPage.Key); + Assert.AreEqual(PublishedTextPage.ContentTypeKey, textPage.ContentType.Key); + Assert.AreEqual(PublishedTextPage.InvariantName, textPage.Name); + }); + + AssertProperties(PublishedTextPage.InvariantProperties, textPage.Properties); + } + private void AssertProperties(IEnumerable propertyCollection, IEnumerable publishedProperties) { foreach (var prop in propertyCollection) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs index 8f06be20c3..34e69c0344 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -24,12 +25,12 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest private string _invariantTitleAlias = "invariantTitle"; private string _invariantTitleName = "Invariant Title"; - private IContentTypeService ContentTypeService => GetRequiredService(); - private ILanguageService LanguageService => GetRequiredService(); private IContentEditingService ContentEditingService => GetRequiredService(); + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); @@ -49,31 +50,35 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest var updatedInvariantTitle = "Updated Invariant Title"; var updatedVariantTitle = "Updated Variant Title"; - var updateModel = new ContentUpdateModel { - InvariantProperties = new[] - { - new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } - }, - Variants = new [] + InvariantProperties = + new[] { new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } }, + Variants = new[] { new VariantModel { Culture = _englishIsoCode, Name = "Updated English Name", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } - } + Properties = + new[] + { + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + } + }, }, new VariantModel { Culture = _danishIsoCode, Name = "Updated Danish Name", - Properties = new [] + Properties = new[] { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + }, }, }, }, @@ -100,28 +105,29 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest var updatedInvariantTitle = "Updated Invariant Title"; var updatedVariantTitle = "Updated Invariant Title"; - var updateModel = new ContentUpdateModel { - InvariantProperties = new[] - { - new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } - }, - Variants = new [] + InvariantProperties = + new[] { new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } }, + Variants = new[] { new VariantModel { Culture = _englishIsoCode, Name = "Updated English Name", - Properties = new [] + Properties = new[] { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } - } + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + }, + }, }, }, }; - var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); + var result = + await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); // Act @@ -134,59 +140,42 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest Assert.AreEqual(_variantTitleName, textPage.Value(_variantTitleAlias, _danishIsoCode)); } - private async Task CreateTestData() { - // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. var language = new LanguageBuilder() .WithCultureInfo(_danishIsoCode) .Build(); - await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey); - var contentType = new ContentTypeBuilder() + var groupKey = Guid.NewGuid(); + var contentType = new ContentTypeEditingBuilder() .WithAlias("cultureVariationTest") .WithName("Culture Variation Test") - .WithContentVariation(ContentVariation.Culture) + .WithAllowAtRoot(true) + .WithVariesByCulture(true) .AddPropertyType() - .WithAlias(_variantTitleAlias) - .WithName(_variantTitleName) - .WithVariations(ContentVariation.Culture) - .Done() + .WithAlias(_variantTitleAlias) + .WithName(_variantTitleName) + .WithVariesByCulture(true) + .WithContainerKey(groupKey) + .Done() .AddPropertyType() - .WithAlias(_invariantTitleAlias) - .WithName(_invariantTitleName) - .WithVariations(ContentVariation.Nothing) - .Done() + .WithAlias(_invariantTitleAlias) + .WithName(_invariantTitleName) + .WithContainerKey(groupKey) + .Done() + .AddPropertyGroup() + .WithName("content") + .WithKey(groupKey) + .Done() .Build(); - contentType.AllowedAsRoot = true; - ContentTypeService.Save(contentType); - var rootContentCreateModel = new ContentCreateModel + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentType, Constants.Security.SuperUserKey); + if (!contentTypeAttempt.Success) { - ContentTypeKey = contentType.Key, - Variants = new[] - { - new VariantModel - { - Culture = "en-US", - Name = "English Page", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } - }, - }, - new VariantModel - { - Culture = "da-DK", - Name = "Danish Page", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } - }, - }, - }, - }; + throw new Exception("Failed to create content type"); + } + var rootContentCreateModel = ContentEditingBuilder.CreateContentWithTwoVariantProperties(contentTypeAttempt.Result.Key, "en-US", "da-DK", _variantTitleAlias, _variantTitleName); var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); VariantPage = result.Result.Content; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs new file mode 100644 index 0000000000..fef4486863 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +public class DocumentBreadthFirstKeyProviderTests +{ + + [Test] + public void ZeroSeedCountReturnsZeroKeys() + { + // The structure here doesn't matter greatly, it just matters that there is something. + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(It.IsAny(), out rootChildren)).Returns(true); + + + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 0 }; + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.Zero(result.Count); + } + + [Test] + public void OnlyReturnsKeysUpToSeedCount() + { + // Structure + // Root + // - Child1 + // - Child2 + // - Child3 + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true); + + var expected = 3; + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = expected }; + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.That(result.Count, Is.EqualTo(expected)); + } + + [Test] + public void IsBreadthFirst() + { + // Structure + // Root + // - Child1 + // - GrandChild + // - Child2 + // - Child3 + + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + var child1Key = Guid.NewGuid(); + var grandChildKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { child1Key, Guid.NewGuid(), Guid.NewGuid() }; + IEnumerable grandChildren = new List { grandChildKey }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(child1Key, out grandChildren)).Returns(true); + + // This'll get all children but no grandchildren + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 4 }; + + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.That(result.Contains(grandChildKey), Is.False); + } + + [Test] + public void CanGetAll() + { + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + + + IEnumerable rootKeyList = new List { rootKey }; + var childrenCount = 300; + List rootChildren = new List (); + for (int i = 0; i < childrenCount; i++) + { + rootChildren.Add(Guid.NewGuid()); + } + + IEnumerable childrenEnumerable = rootChildren; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out childrenEnumerable)).Returns(true); + var settings = new CacheSettings { DocumentBreadthFirstSeedCount = int.MaxValue }; + + + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(settings)); + + var result = sut.GetSeedKeys(); + + var expected = childrenCount + 1; // Root + children + Assert.That(result.Count, Is.EqualTo(expected)); + } +}