diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs index 9dcf85b6ef..0eeeb4d6da 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs @@ -16,12 +16,18 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService private readonly IPublishedMediaCache _publishedMediaCache; private readonly ILogger _logger; private readonly IMediaNavigationQueryService _mediaNavigationQueryService; + private readonly IPublishedMediaStatusFilteringService _publishedMediaStatusFilteringService; - public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger logger, IMediaNavigationQueryService mediaNavigationQueryService) + public ApiMediaQueryService( + IPublishedMediaCache publishedMediaCache, + ILogger logger, + IMediaNavigationQueryService mediaNavigationQueryService, + IPublishedMediaStatusFilteringService publishedMediaStatusFilteringService) { _publishedMediaCache = publishedMediaCache; _logger = logger; _mediaNavigationQueryService = mediaNavigationQueryService; + _publishedMediaStatusFilteringService = publishedMediaStatusFilteringService; } /// @@ -71,7 +77,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService break; } - currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService); + currentChildren = resolvedMedia.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService); } return resolvedMedia; @@ -104,7 +110,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService ? mediaCache.GetById(parentKey) : TryGetByPath(childrenOf, mediaCache); - return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty(); + return parent?.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService) ?? Array.Empty(); } private IEnumerable? ApplyFilters(IEnumerable source, IEnumerable filters) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs index 3f22c3931c..b491b45f59 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs @@ -1,12 +1,13 @@ using System.Diagnostics; using System.Linq.Expressions; -using System.Runtime.Versioning; using System.Text; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.ViewModels.Template.Query; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Models.TemplateQuery; using Umbraco.Cms.Core.PublishedCache; @@ -20,14 +21,46 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Query; public class ExecuteTemplateQueryController : TemplateQueryControllerBase { private readonly IPublishedContentQuery _publishedContentQuery; - private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IContentTypeService _contentTypeService; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _documentNavigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private static readonly string _indent = $"{Environment.NewLine} "; + public ExecuteTemplateQueryController( + IPublishedContentQuery publishedContentQuery, + IPublishedValueFallback publishedValueFallback, + IContentTypeService contentTypeService, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + { + _publishedContentQuery = publishedContentQuery; + _publishedValueFallback = publishedValueFallback; + _contentTypeService = contentTypeService; + _documentNavigationQueryService = documentNavigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] + public ExecuteTemplateQueryController( + IPublishedContentQuery publishedContentQuery, + IVariationContextAccessor variationContextAccessor, + IPublishedValueFallback publishedValueFallback, + IContentTypeService contentTypeService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + publishedContentQuery, + publishedValueFallback, + contentTypeService, + documentNavigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public ExecuteTemplateQueryController( IPublishedContentQuery publishedContentQuery, IVariationContextAccessor variationContextAccessor, @@ -35,13 +68,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase IContentTypeService contentTypeService, IPublishedContentCache contentCache, IDocumentNavigationQueryService documentNavigationQueryService) + : this( + publishedContentQuery, + publishedValueFallback, + contentTypeService, + documentNavigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) { - _publishedContentQuery = publishedContentQuery; - _variationContextAccessor = variationContextAccessor; - _publishedValueFallback = publishedValueFallback; - _contentTypeService = contentTypeService; - _contentCache = contentCache; - _documentNavigationQueryService = documentNavigationQueryService; } [HttpPost("execute")] @@ -118,13 +151,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase queryExpression.Append(".ChildrenOfType(\"").Append(model.DocumentTypeAlias).Append("\")"); return rootContent == null ? Enumerable.Empty() - : rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias); + : rootContent.ChildrenOfType(_documentNavigationQueryService, _publishedContentStatusFilteringService, model.DocumentTypeAlias); } queryExpression.Append(".Children()"); return rootContent == null ? Enumerable.Empty() - : rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService); + : rootContent.Children(_documentNavigationQueryService, _publishedContentStatusFilteringService); } private IEnumerable ApplyFiltering(IEnumerable? filters, IEnumerable contentQuery, StringBuilder queryExpression) diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index c5d93d979c..1868ee0aea 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -42,7 +42,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder requestSettings.OnChange(settings => _requestSettings = settings); } - [Obsolete("Use constructor that takes an IPublishStatusQueryService instead, scheduled for removal in v17")] + [Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")] public ApiContentRouteBuilder( IApiContentPathProvider apiContentPathProvider, IOptions globalSettings, @@ -80,7 +80,12 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder contentPath = contentPath.EnsureStartsWith("/"); - IPublishedContent root = GetRoot(content, isPreview); + IPublishedContent? root = GetRoot(content, isPreview); + if (root is null) + { + return null; + } + var rootPath = root.UrlSegment(_variationContextAccessor, culture) ?? string.Empty; if (_globalSettings.HideTopLevelNodeFromPath == false) @@ -127,19 +132,21 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private static bool IsInvalidContentPath(string? path) => path.IsNullOrWhiteSpace() || "#".Equals(path); - private IPublishedContent GetRoot(IPublishedContent content, bool isPreview) + private IPublishedContent? GetRoot(IPublishedContent content, bool isPreview) { - if (isPreview is false) + if (content.Level == 1) { - return content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService); + return content; } - _navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); - IEnumerable rootContent = rootKeys.Select(x => _contentCache.GetById(true, x)).WhereNotNull(); + if (_navigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable ancestorKeys) is false) + { + return null; + } - // in very edge case scenarios during preview, content.Root() does not map to the root. - // we'll code our way around it for the time being. - return rootContent.FirstOrDefault(root => root.IsAncestorOrSelf(content)) - ?? content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService); + Guid[] ancestorKeysAsArray = ancestorKeys as Guid[] ?? ancestorKeys.ToArray(); + return ancestorKeysAsArray.Length > 0 + ? _contentCache.GetById(isPreview, ancestorKeysAsArray.Last()) + : content; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 95c6da574a..3d36c67d3a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -387,6 +387,9 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(x => x.GetRequiredService()); Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(); + Services.AddUnique(); + // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index bd5c490511..1fb0445bb1 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -113,6 +113,14 @@ Mange hilsner fra Umbraco robotten %1% mere.]]> %1% for mange.]]> Ét eller flere områder lever ikke op til kravene for antal indholdselementer. + Den valgte medie type er ugyldig. + Det er kun tilladt at vælge ét medie. + Valgt medie kommer fra en ugyldig mappe. + Værdien %0% er mindre end det tilladte minimum af %1%. + Værdien %0% er større end det tilladte maksimum af %1%. + Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%. + Værdien %0% forventes ikke at indeholde et spænd. + Værdien %0% forventes at have en værdi der er større end fra værdien. Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} @@ -124,4 +132,4 @@ Mange hilsner fra Umbraco robotten Filskrivning Mediemappeoprettelse - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index c05ea72cfc..e1fdba85d4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -387,6 +387,14 @@ User group name '%0%' is already taken Member group name '%0%' is already taken Username '%0%' is already taken + The value %0% is less than the allowed minimum value of %1% + The value %0% is greater than the allowed maximum value of %1% + The value %0% does not correspond with the configured step value of %1% and minimum value of %2% + The value %0% is not expected to contain a range + The value %0% is not expected to have a to value less than the from value + The chosen media type is invalid. + Multiple selected media is not allowed. + The selected media is from the wrong folder. )?<\/umb-rte-block(?:-inline)?>/gi, ); let blockElement: RegExpExecArray | null; - while ((blockElement = regex.exec(value)) !== null) { + while ((blockElement = regex.exec(markup)) !== null) { if (blockElement.groups?.key) { usedContentKeys.push(blockElement.groups.key); } } - this._filterUnusedBlocks(usedContentKeys); - - this._latestMarkup = value; - if (this.value) { this.value = { ...this.value, - markup: this._latestMarkup, + markup: markup, }; } else { this.value = { - markup: this._latestMarkup, + markup: markup, blocks: { layout: {}, contentData: [], @@ -55,6 +57,9 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem }; } + // lets run this one after we set the value, to make sure we don't reset the value. + this._filterUnusedBlocks(usedContentKeys); + this._fireChangeEvent(); } @@ -64,6 +69,8 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem .configuration=${this._config} .value=${this._markup} ?readonly=${this.readonly} + ?required=${this.mandatory} + ?required-message=${this.mandatoryMessage} @change=${this.#onChange}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts index f39f855037..3bae91ba41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts @@ -1,17 +1,20 @@ import { UMB_USER_GROUP_COLLECTION_ALIAS } from '../collection/index.js'; +import { UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, UMB_USER_GROUP_ITEM_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_USER_GROUP_ENTITY_TYPE } from '../entity.js'; +import { UMB_ENTITY_BULK_ACTION_DELETE_KIND } from '@umbraco-cms/backoffice/entity-bulk-action'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; export const manifests: Array = [ { type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_DELETE_KIND, alias: 'Umb.EntityBulkAction.UserGroup.Delete', name: 'Delete User Group Entity Bulk Action', weight: 400, - api: () => import('./delete/delete.action.js'), forEntityTypes: [UMB_USER_GROUP_ENTITY_TYPE], meta: { - label: 'Delete', + itemRepositoryAlias: UMB_USER_GROUP_ITEM_REPOSITORY_ALIAS, + detailRepositoryAlias: UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts index aae6d579e9..e44a4a6a7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts @@ -244,7 +244,9 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
- +
diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index da094f92b1..512ce70e9a 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -74,6 +74,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "./src/packages/core/entity-create-option-action/index.ts" ], "@umbraco-cms/backoffice/entity": ["./src/packages/core/entity/index.ts"], + "@umbraco-cms/backoffice/entity-item": ["./src/packages/core/entity-item/index.ts"], "@umbraco-cms/backoffice/event": ["./src/packages/core/event/index.ts"], "@umbraco-cms/backoffice/extension-registry": ["./src/packages/core/extension-registry/index.ts"], "@umbraco-cms/backoffice/health-check": ["./src/packages/health-check/index.ts"], diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts index 4d0720208b..0cd1f38783 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts @@ -38,71 +38,72 @@ import * as import35 from '@umbraco-cms/backoffice/entity-action'; import * as import36 from '@umbraco-cms/backoffice/entity-bulk-action'; import * as import37 from '@umbraco-cms/backoffice/entity-create-option-action'; import * as import38 from '@umbraco-cms/backoffice/entity'; -import * as import39 from '@umbraco-cms/backoffice/event'; -import * as import40 from '@umbraco-cms/backoffice/extension-registry'; -import * as import41 from '@umbraco-cms/backoffice/health-check'; -import * as import42 from '@umbraco-cms/backoffice/help'; -import * as import43 from '@umbraco-cms/backoffice/icon'; -import * as import44 from '@umbraco-cms/backoffice/id'; -import * as import45 from '@umbraco-cms/backoffice/imaging'; -import * as import46 from '@umbraco-cms/backoffice/language'; -import * as import47 from '@umbraco-cms/backoffice/lit-element'; -import * as import48 from '@umbraco-cms/backoffice/localization'; -import * as import49 from '@umbraco-cms/backoffice/log-viewer'; -import * as import50 from '@umbraco-cms/backoffice/media-type'; -import * as import51 from '@umbraco-cms/backoffice/media'; -import * as import52 from '@umbraco-cms/backoffice/member-group'; -import * as import53 from '@umbraco-cms/backoffice/member-type'; -import * as import54 from '@umbraco-cms/backoffice/member'; -import * as import55 from '@umbraco-cms/backoffice/menu'; -import * as import56 from '@umbraco-cms/backoffice/modal'; -import * as import57 from '@umbraco-cms/backoffice/multi-url-picker'; -import * as import58 from '@umbraco-cms/backoffice/notification'; -import * as import59 from '@umbraco-cms/backoffice/object-type'; -import * as import60 from '@umbraco-cms/backoffice/package'; -import * as import61 from '@umbraco-cms/backoffice/partial-view'; -import * as import62 from '@umbraco-cms/backoffice/picker-input'; -import * as import63 from '@umbraco-cms/backoffice/picker'; -import * as import64 from '@umbraco-cms/backoffice/property-action'; -import * as import65 from '@umbraco-cms/backoffice/property-editor'; -import * as import66 from '@umbraco-cms/backoffice/property-type'; -import * as import67 from '@umbraco-cms/backoffice/property'; -import * as import68 from '@umbraco-cms/backoffice/recycle-bin'; -import * as import69 from '@umbraco-cms/backoffice/relation-type'; -import * as import70 from '@umbraco-cms/backoffice/relations'; -import * as import71 from '@umbraco-cms/backoffice/repository'; -import * as import72 from '@umbraco-cms/backoffice/resources'; -import * as import73 from '@umbraco-cms/backoffice/router'; -import * as import74 from '@umbraco-cms/backoffice/rte'; -import * as import75 from '@umbraco-cms/backoffice/script'; -import * as import76 from '@umbraco-cms/backoffice/search'; -import * as import77 from '@umbraco-cms/backoffice/section'; -import * as import78 from '@umbraco-cms/backoffice/server-file-system'; -import * as import79 from '@umbraco-cms/backoffice/settings'; -import * as import80 from '@umbraco-cms/backoffice/sorter'; -import * as import81 from '@umbraco-cms/backoffice/static-file'; -import * as import82 from '@umbraco-cms/backoffice/store'; -import * as import83 from '@umbraco-cms/backoffice/style'; -import * as import84 from '@umbraco-cms/backoffice/stylesheet'; -import * as import85 from '@umbraco-cms/backoffice/sysinfo'; -import * as import86 from '@umbraco-cms/backoffice/tags'; -import * as import87 from '@umbraco-cms/backoffice/template'; -import * as import88 from '@umbraco-cms/backoffice/temporary-file'; -import * as import89 from '@umbraco-cms/backoffice/themes'; -import * as import90 from '@umbraco-cms/backoffice/tiny-mce'; -import * as import91 from '@umbraco-cms/backoffice/tiptap'; -import * as import92 from '@umbraco-cms/backoffice/translation'; -import * as import93 from '@umbraco-cms/backoffice/tree'; -import * as import94 from '@umbraco-cms/backoffice/ufm'; -import * as import95 from '@umbraco-cms/backoffice/user-change-password'; -import * as import96 from '@umbraco-cms/backoffice/user-group'; -import * as import97 from '@umbraco-cms/backoffice/user-permission'; -import * as import98 from '@umbraco-cms/backoffice/user'; -import * as import99 from '@umbraco-cms/backoffice/utils'; -import * as import100 from '@umbraco-cms/backoffice/validation'; -import * as import101 from '@umbraco-cms/backoffice/variant'; -import * as import102 from '@umbraco-cms/backoffice/webhook'; -import * as import103 from '@umbraco-cms/backoffice/workspace'; +import * as import39 from '@umbraco-cms/backoffice/entity-item'; +import * as import40 from '@umbraco-cms/backoffice/event'; +import * as import41 from '@umbraco-cms/backoffice/extension-registry'; +import * as import42 from '@umbraco-cms/backoffice/health-check'; +import * as import43 from '@umbraco-cms/backoffice/help'; +import * as import44 from '@umbraco-cms/backoffice/icon'; +import * as import45 from '@umbraco-cms/backoffice/id'; +import * as import46 from '@umbraco-cms/backoffice/imaging'; +import * as import47 from '@umbraco-cms/backoffice/language'; +import * as import48 from '@umbraco-cms/backoffice/lit-element'; +import * as import49 from '@umbraco-cms/backoffice/localization'; +import * as import50 from '@umbraco-cms/backoffice/log-viewer'; +import * as import51 from '@umbraco-cms/backoffice/media-type'; +import * as import52 from '@umbraco-cms/backoffice/media'; +import * as import53 from '@umbraco-cms/backoffice/member-group'; +import * as import54 from '@umbraco-cms/backoffice/member-type'; +import * as import55 from '@umbraco-cms/backoffice/member'; +import * as import56 from '@umbraco-cms/backoffice/menu'; +import * as import57 from '@umbraco-cms/backoffice/modal'; +import * as import58 from '@umbraco-cms/backoffice/multi-url-picker'; +import * as import59 from '@umbraco-cms/backoffice/notification'; +import * as import60 from '@umbraco-cms/backoffice/object-type'; +import * as import61 from '@umbraco-cms/backoffice/package'; +import * as import62 from '@umbraco-cms/backoffice/partial-view'; +import * as import63 from '@umbraco-cms/backoffice/picker-input'; +import * as import64 from '@umbraco-cms/backoffice/picker'; +import * as import65 from '@umbraco-cms/backoffice/property-action'; +import * as import66 from '@umbraco-cms/backoffice/property-editor'; +import * as import67 from '@umbraco-cms/backoffice/property-type'; +import * as import68 from '@umbraco-cms/backoffice/property'; +import * as import69 from '@umbraco-cms/backoffice/recycle-bin'; +import * as import70 from '@umbraco-cms/backoffice/relation-type'; +import * as import71 from '@umbraco-cms/backoffice/relations'; +import * as import72 from '@umbraco-cms/backoffice/repository'; +import * as import73 from '@umbraco-cms/backoffice/resources'; +import * as import74 from '@umbraco-cms/backoffice/router'; +import * as import75 from '@umbraco-cms/backoffice/rte'; +import * as import76 from '@umbraco-cms/backoffice/script'; +import * as import77 from '@umbraco-cms/backoffice/search'; +import * as import78 from '@umbraco-cms/backoffice/section'; +import * as import79 from '@umbraco-cms/backoffice/server-file-system'; +import * as import80 from '@umbraco-cms/backoffice/settings'; +import * as import81 from '@umbraco-cms/backoffice/sorter'; +import * as import82 from '@umbraco-cms/backoffice/static-file'; +import * as import83 from '@umbraco-cms/backoffice/store'; +import * as import84 from '@umbraco-cms/backoffice/style'; +import * as import85 from '@umbraco-cms/backoffice/stylesheet'; +import * as import86 from '@umbraco-cms/backoffice/sysinfo'; +import * as import87 from '@umbraco-cms/backoffice/tags'; +import * as import88 from '@umbraco-cms/backoffice/template'; +import * as import89 from '@umbraco-cms/backoffice/temporary-file'; +import * as import90 from '@umbraco-cms/backoffice/themes'; +import * as import91 from '@umbraco-cms/backoffice/tiny-mce'; +import * as import92 from '@umbraco-cms/backoffice/tiptap'; +import * as import93 from '@umbraco-cms/backoffice/translation'; +import * as import94 from '@umbraco-cms/backoffice/tree'; +import * as import95 from '@umbraco-cms/backoffice/ufm'; +import * as import96 from '@umbraco-cms/backoffice/user-change-password'; +import * as import97 from '@umbraco-cms/backoffice/user-group'; +import * as import98 from '@umbraco-cms/backoffice/user-permission'; +import * as import99 from '@umbraco-cms/backoffice/user'; +import * as import100 from '@umbraco-cms/backoffice/utils'; +import * as import101 from '@umbraco-cms/backoffice/validation'; +import * as import102 from '@umbraco-cms/backoffice/variant'; +import * as import103 from '@umbraco-cms/backoffice/webhook'; +import * as import104 from '@umbraco-cms/backoffice/workspace'; export const imports = [ { @@ -262,264 +263,268 @@ import * as import103 from '@umbraco-cms/backoffice/workspace'; package: import38 }, { - path: '@umbraco-cms/backoffice/event', + path: '@umbraco-cms/backoffice/entity-item', package: import39 }, { - path: '@umbraco-cms/backoffice/extension-registry', + path: '@umbraco-cms/backoffice/event', package: import40 }, { - path: '@umbraco-cms/backoffice/health-check', + path: '@umbraco-cms/backoffice/extension-registry', package: import41 }, { - path: '@umbraco-cms/backoffice/help', + path: '@umbraco-cms/backoffice/health-check', package: import42 }, { - path: '@umbraco-cms/backoffice/icon', + path: '@umbraco-cms/backoffice/help', package: import43 }, { - path: '@umbraco-cms/backoffice/id', + path: '@umbraco-cms/backoffice/icon', package: import44 }, { - path: '@umbraco-cms/backoffice/imaging', + path: '@umbraco-cms/backoffice/id', package: import45 }, { - path: '@umbraco-cms/backoffice/language', + path: '@umbraco-cms/backoffice/imaging', package: import46 }, { - path: '@umbraco-cms/backoffice/lit-element', + path: '@umbraco-cms/backoffice/language', package: import47 }, { - path: '@umbraco-cms/backoffice/localization', + path: '@umbraco-cms/backoffice/lit-element', package: import48 }, { - path: '@umbraco-cms/backoffice/log-viewer', + path: '@umbraco-cms/backoffice/localization', package: import49 }, { - path: '@umbraco-cms/backoffice/media-type', + path: '@umbraco-cms/backoffice/log-viewer', package: import50 }, { - path: '@umbraco-cms/backoffice/media', + path: '@umbraco-cms/backoffice/media-type', package: import51 }, { - path: '@umbraco-cms/backoffice/member-group', + path: '@umbraco-cms/backoffice/media', package: import52 }, { - path: '@umbraco-cms/backoffice/member-type', + path: '@umbraco-cms/backoffice/member-group', package: import53 }, { - path: '@umbraco-cms/backoffice/member', + path: '@umbraco-cms/backoffice/member-type', package: import54 }, { - path: '@umbraco-cms/backoffice/menu', + path: '@umbraco-cms/backoffice/member', package: import55 }, { - path: '@umbraco-cms/backoffice/modal', + path: '@umbraco-cms/backoffice/menu', package: import56 }, { - path: '@umbraco-cms/backoffice/multi-url-picker', + path: '@umbraco-cms/backoffice/modal', package: import57 }, { - path: '@umbraco-cms/backoffice/notification', + path: '@umbraco-cms/backoffice/multi-url-picker', package: import58 }, { - path: '@umbraco-cms/backoffice/object-type', + path: '@umbraco-cms/backoffice/notification', package: import59 }, { - path: '@umbraco-cms/backoffice/package', + path: '@umbraco-cms/backoffice/object-type', package: import60 }, { - path: '@umbraco-cms/backoffice/partial-view', + path: '@umbraco-cms/backoffice/package', package: import61 }, { - path: '@umbraco-cms/backoffice/picker-input', + path: '@umbraco-cms/backoffice/partial-view', package: import62 }, { - path: '@umbraco-cms/backoffice/picker', + path: '@umbraco-cms/backoffice/picker-input', package: import63 }, { - path: '@umbraco-cms/backoffice/property-action', + path: '@umbraco-cms/backoffice/picker', package: import64 }, { - path: '@umbraco-cms/backoffice/property-editor', + path: '@umbraco-cms/backoffice/property-action', package: import65 }, { - path: '@umbraco-cms/backoffice/property-type', + path: '@umbraco-cms/backoffice/property-editor', package: import66 }, { - path: '@umbraco-cms/backoffice/property', + path: '@umbraco-cms/backoffice/property-type', package: import67 }, { - path: '@umbraco-cms/backoffice/recycle-bin', + path: '@umbraco-cms/backoffice/property', package: import68 }, { - path: '@umbraco-cms/backoffice/relation-type', + path: '@umbraco-cms/backoffice/recycle-bin', package: import69 }, { - path: '@umbraco-cms/backoffice/relations', + path: '@umbraco-cms/backoffice/relation-type', package: import70 }, { - path: '@umbraco-cms/backoffice/repository', + path: '@umbraco-cms/backoffice/relations', package: import71 }, { - path: '@umbraco-cms/backoffice/resources', + path: '@umbraco-cms/backoffice/repository', package: import72 }, { - path: '@umbraco-cms/backoffice/router', + path: '@umbraco-cms/backoffice/resources', package: import73 }, { - path: '@umbraco-cms/backoffice/rte', + path: '@umbraco-cms/backoffice/router', package: import74 }, { - path: '@umbraco-cms/backoffice/script', + path: '@umbraco-cms/backoffice/rte', package: import75 }, { - path: '@umbraco-cms/backoffice/search', + path: '@umbraco-cms/backoffice/script', package: import76 }, { - path: '@umbraco-cms/backoffice/section', + path: '@umbraco-cms/backoffice/search', package: import77 }, { - path: '@umbraco-cms/backoffice/server-file-system', + path: '@umbraco-cms/backoffice/section', package: import78 }, { - path: '@umbraco-cms/backoffice/settings', + path: '@umbraco-cms/backoffice/server-file-system', package: import79 }, { - path: '@umbraco-cms/backoffice/sorter', + path: '@umbraco-cms/backoffice/settings', package: import80 }, { - path: '@umbraco-cms/backoffice/static-file', + path: '@umbraco-cms/backoffice/sorter', package: import81 }, { - path: '@umbraco-cms/backoffice/store', + path: '@umbraco-cms/backoffice/static-file', package: import82 }, { - path: '@umbraco-cms/backoffice/style', + path: '@umbraco-cms/backoffice/store', package: import83 }, { - path: '@umbraco-cms/backoffice/stylesheet', + path: '@umbraco-cms/backoffice/style', package: import84 }, { - path: '@umbraco-cms/backoffice/sysinfo', + path: '@umbraco-cms/backoffice/stylesheet', package: import85 }, { - path: '@umbraco-cms/backoffice/tags', + path: '@umbraco-cms/backoffice/sysinfo', package: import86 }, { - path: '@umbraco-cms/backoffice/template', + path: '@umbraco-cms/backoffice/tags', package: import87 }, { - path: '@umbraco-cms/backoffice/temporary-file', + path: '@umbraco-cms/backoffice/template', package: import88 }, { - path: '@umbraco-cms/backoffice/themes', + path: '@umbraco-cms/backoffice/temporary-file', package: import89 }, { - path: '@umbraco-cms/backoffice/tiny-mce', + path: '@umbraco-cms/backoffice/themes', package: import90 }, { - path: '@umbraco-cms/backoffice/tiptap', + path: '@umbraco-cms/backoffice/tiny-mce', package: import91 }, { - path: '@umbraco-cms/backoffice/translation', + path: '@umbraco-cms/backoffice/tiptap', package: import92 }, { - path: '@umbraco-cms/backoffice/tree', + path: '@umbraco-cms/backoffice/translation', package: import93 }, { - path: '@umbraco-cms/backoffice/ufm', + path: '@umbraco-cms/backoffice/tree', package: import94 }, { - path: '@umbraco-cms/backoffice/user-change-password', + path: '@umbraco-cms/backoffice/ufm', package: import95 }, { - path: '@umbraco-cms/backoffice/user-group', + path: '@umbraco-cms/backoffice/user-change-password', package: import96 }, { - path: '@umbraco-cms/backoffice/user-permission', + path: '@umbraco-cms/backoffice/user-group', package: import97 }, { - path: '@umbraco-cms/backoffice/user', + path: '@umbraco-cms/backoffice/user-permission', package: import98 }, { - path: '@umbraco-cms/backoffice/utils', + path: '@umbraco-cms/backoffice/user', package: import99 }, { - path: '@umbraco-cms/backoffice/validation', + path: '@umbraco-cms/backoffice/utils', package: import100 }, { - path: '@umbraco-cms/backoffice/variant', + path: '@umbraco-cms/backoffice/validation', package: import101 }, { - path: '@umbraco-cms/backoffice/webhook', + path: '@umbraco-cms/backoffice/variant', package: import102 }, { - path: '@umbraco-cms/backoffice/workspace', + path: '@umbraco-cms/backoffice/webhook', package: import103 + }, +{ + path: '@umbraco-cms/backoffice/workspace', + package: import104 } ]; \ No newline at end of file diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index 57666cbc4f..ecad1a3484 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -23,8 +23,8 @@ public class UmbLoginController : SurfaceController private readonly IMemberManager _memberManager; private readonly IMemberSignInManager _signInManager; private readonly ITwoFactorLoginService _twoFactorLoginService; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; [ActivatorUtilitiesConstructor] public UmbLoginController( @@ -37,18 +37,75 @@ public class UmbLoginController : SurfaceController IMemberSignInManager signInManager, IMemberManager memberManager, ITwoFactorLoginService twoFactorLoginService, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) { _signInManager = signInManager; _memberManager = memberManager; _twoFactorLoginService = twoFactorLoginService; - _contentCache = contentCache; _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + memberManager, + twoFactorLoginService, + navigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + memberManager, + twoFactorLoginService, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public UmbLoginController( IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, @@ -69,8 +126,8 @@ public class UmbLoginController : SurfaceController signInManager, memberManager, twoFactorLoginService, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -102,7 +159,7 @@ public class UmbLoginController : SurfaceController // If it's not a local URL we'll redirect to the root of the current site. return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage!.AncestorOrSelf(_contentCache, _navigationQueryService, 1)!.Url(PublishedUrlProvider)); + : CurrentPage!.AncestorOrSelf(_navigationQueryService, _publishedContentStatusFilteringService, 1)!.Url(PublishedUrlProvider)); } // Redirect to current URL by default. diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index d0e3899ceb..8ea239c327 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.21", + "@umbraco/playwright-testhelpers": "^15.0.24", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -58,20 +58,20 @@ } }, "node_modules/@umbraco/json-models-builders": { - "version": "2.0.29", - "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.29.tgz", - "integrity": "sha512-cD20lBDhuLmo2hJk7Ap5hfeDejJhmqjMsANazXgKCulHt5J4HgI3VayXPJx/SyUnCtfc8dylZASOq+2cPzukpA==", + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.30.tgz", + "integrity": "sha512-mOXhbvi3mfpkueDaKkMAGKksieB7Pb/pe8e7Hwz3cYoWUQCdGTzlx2ffps6l8LpkRTXZ6IazGg3OwpVFaedDzQ==", + "license": "MIT", "dependencies": { "camelize": "^1.0.1" } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.21", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.21.tgz", - "integrity": "sha512-R5b59dLMJiCB7fEUz3zyKBjF1XetII1FspkpL6Q1INEz0OBzibU9oKKjueMj/HxrhpeszK+8I3OrfZ87mYwuTQ==", - "license": "MIT", + "version": "15.0.24", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.24.tgz", + "integrity": "sha512-cv7sr3e1vhOoqAKOgj82kKgWY9dCQCnQdP+4rGllM/Dhvup+nSs93XKOAnTc2Fn3ZqhpwA8PDL8Pg9riUpt5JQ==", "dependencies": { - "@umbraco/json-models-builders": "2.0.29", + "@umbraco/json-models-builders": "2.0.30", "node-fetch": "^2.6.7" } }, diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 86227ec7c3..9c542f9bf0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.21", + "@umbraco/playwright-testhelpers": "^15.0.24", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index df67c034ab..24e84cb79e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -29,7 +29,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickInfoTab(); - await umbracoUi.content.doesLinkHaveText(notPublishContentLink); + await umbracoUi.content.doesDocumentHaveLink(notPublishContentLink); await umbracoUi.content.clickSaveAndPublishButton(); // Assert @@ -46,7 +46,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi hour12: true, }); await umbracoUi.content.doesCreatedDateHaveText(expectedCreatedDate); - await umbracoUi.content.doesLinkHaveText(contentData.urls[0].url ? contentData.urls[0].url : '/'); + await umbracoUi.content.doesDocumentHaveLink(contentData.urls[0].url ? contentData.urls[0].url : '/'); // TODO: Uncomment this when front-end is ready. Currently the publication status of content is not changed to "Published" immediately after publishing it //await umbracoUi.content.doesPublicationStatusHaveText(contentData.variants[0].state === 'Draft' ? 'Unpublished' : contentData.variants[0].state); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts index de5dccd627..fccdb0f9de 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts @@ -132,7 +132,8 @@ test('can delete block element in the content', async ({umbracoApi, umbracoUi}) expect(blockGridValue).toBeFalsy(); }); -test('cannot add number of block element greater than the maximum amount', async ({umbracoApi, umbracoUi}) => { +// Skip this flaky tests as sometimes the modal to choose block item is not displayed +test.skip('cannot add number of block element greater than the maximum amount', async ({umbracoApi, umbracoUi}) => { // Arrange const customDataTypeId = await umbracoApi.dataType.createBlockListWithABlockAndMinAndMaxAmount(customDataTypeName, elementTypeId, 0, 1); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); @@ -154,7 +155,8 @@ test('cannot add number of block element greater than the maximum amount', async await umbracoUi.content.doesFormValidationMessageContainText('too many'); }); -test('can set the label of block element in the content', async ({umbracoApi, umbracoUi}) => { +// Skip this flaky tests as sometimes the modal to choose block item is not displayed +test.skip('can set the label of block element in the content', async ({umbracoApi, umbracoUi}) => { // Arrange const blockLabel = 'Test Block Label'; const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithLabel(customDataTypeName, elementTypeId, blockLabel); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts index b18d620f20..28710ac5c1 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts @@ -150,10 +150,13 @@ test('can import a dictionary item with descendants', {tag: '@smoke'}, async ({u await umbracoUi.dictionary.clickActionsMenuForDictionary(dictionaryName); await umbracoUi.dictionary.clickImportButton(); await umbracoUi.dictionary.importDictionary(udtFilePath); + // These timeouts are necessary as this test can fail + await umbracoUi.waitForTimeout(500); // Assert // Verify the imported dictionary items display in the list await umbracoUi.reloadPage(); + await umbracoUi.waitForTimeout(500); expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importParentDictionaryName)).toBeTruthy(); expect(await umbracoUi.dictionary.doesDictionaryListHaveText(importChildDictionaryName)).toBeTruthy(); // Verify the imported dictionary items display in the tree diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts index 67cd958a48..c56fa1675c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts @@ -3,7 +3,7 @@ import {expect} from "@playwright/test"; const allPermissions = { uiPermission: - ['Browse Node', + ['Browse', 'Create Document Blueprint', 'Delete', 'Create', diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 6d1db8132f..d1ad93da3a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -281,4 +281,47 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT Assert.AreEqual(3, allSiblingsList.Count); }); } + + // a lot of structural querying assumes a specific order of descendants, so let's ensure that. + [Test] + public void Descendants_Are_In_Top_Down_Order_Of_Structure() + { + var result = DocumentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(Root.Key, out IEnumerable descendantsKeys); + Assert.IsTrue(result); + + var descendantsKeysAsArray = descendantsKeys.ToArray(); + Assert.AreEqual(9, descendantsKeysAsArray.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(Root.Key, descendantsKeysAsArray[0]); + Assert.AreEqual(Child1.Key, descendantsKeysAsArray[1]); + Assert.AreEqual(Grandchild1.Key, descendantsKeysAsArray[2]); + Assert.AreEqual(Grandchild2.Key, descendantsKeysAsArray[3]); + Assert.AreEqual(Child2.Key, descendantsKeysAsArray[4]); + Assert.AreEqual(Grandchild3.Key, descendantsKeysAsArray[5]); + Assert.AreEqual(GreatGrandchild1.Key, descendantsKeysAsArray[6]); + Assert.AreEqual(Child3.Key, descendantsKeysAsArray[7]); + Assert.AreEqual(Grandchild4.Key, descendantsKeysAsArray[8]); + }); + } + + // a lot of structural querying assumes a specific order of ancestors, so let's ensure that. + [Test] + public void Ancestors_Are_In_Down_Top_Order() + { + var result = DocumentNavigationQueryService.TryGetAncestorsOrSelfKeys(GreatGrandchild1.Key, out IEnumerable ancestorsKeys); + Assert.IsTrue(result); + + var ancestorKeysAsArray = ancestorsKeys.ToArray(); + Assert.AreEqual(4, ancestorKeysAsArray.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(GreatGrandchild1.Key, ancestorKeysAsArray[0]); + Assert.AreEqual(Grandchild3.Key, ancestorKeysAsArray[1]); + Assert.AreEqual(Child2.Key, ancestorKeysAsArray[2]); + Assert.AreEqual(Root.Key, ancestorKeysAsArray[3]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs index 26a32fe67b..8e8878e9bc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs @@ -512,4 +512,126 @@ internal partial class BlockListElementLevelVariationTests Assert.IsEmpty(result.ValidationErrors); } + + [Test] + public async Task Can_Validate_Properties_Variant_Blocks() + { + var elementType = CreateElementTypeWithValidation(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // blocks property values use null culture for culture variant block editor properties + new() { Alias = "invariantText", Value = "Valid invariantText content value", Culture = null }, + new() { Alias = "variantText", Value = "Invalid variantText content value", Culture = null }, + }, + new List + { + // blocks property values use null culture for culture variant block editor properties + new() { Alias = "invariantText", Value = "Invalid invariantText settings value", Culture = null }, + new() { Alias = "variantText", Value = "Valid variantText settings value", Culture = null }, + }, + "en-US", + null)); + + // make sure all blocks are exposed as they would be for culture variant properties + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = null } + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel + { + Name = "Name en-US", + Culture = "en-US", + Segment = null, + Properties = [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + } + ], + InvariantProperties = [] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(2, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == "en-US" && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[1].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value")); + }); + } + + [Test] + public async Task Can_Validate_Missing_Properties_Variant_Blocks() + { + var elementType = CreateElementTypeWithValidation(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // missing the mandatory "invariantText" + new() { Alias = "variantText", Value = "Valid variantText content value", Culture = null }, + }, + new List + { + // missing the mandatory "variantText" (which, to add to the confusion, is invariant at block level in this test case) + new() { Alias = "invariantText", Value = "Valid invariantText settings value", Culture = null }, + }, + "en-US", + null)); + + // make sure all blocks are exposed as they would be for culture variant properties + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = null } + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel + { + Name = "Name en-US", + Culture = "en-US", + Segment = null, + Properties = [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + } + ], + InvariantProperties = [] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(2, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == "en-US" && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'variantText' && @.culture == null && @.segment == null)].value")); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs index 5370443a5d..b436d7f17c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -148,9 +148,9 @@ internal partial class BlockListElementLevelVariationTests : BlockEditorElementV return GetPublishedContent(content.Key); } - private IContentType CreateElementTypeWithValidation() + private IContentType CreateElementTypeWithValidation(ContentVariation contentVariation = ContentVariation.Culture) { - var elementType = CreateElementType(ContentVariation.Culture); + var elementType = CreateElementType(contentVariation); foreach (var propertyType in elementType.PropertyTypes) { propertyType.Mandatory = true; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 144f987da8..ae1e877e15 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -413,6 +413,6 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest GetRequiredService>(), GetRequiredService(), GetRequiredService(), - GetRequiredService(), - GetRequiredService()).GetAwaiter().GetResult(); + GetRequiredService(), + GetRequiredService()).GetAwaiter().GetResult(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index aa94d9147b..99ccbd7a61 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -36,7 +36,11 @@ public class ContentBuilderTests : DeliveryApiTests .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) .Returns((IPublishedContent c, string? culture) => $"url:{c.UrlSegment}"); - var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings()); + var navigationQueryServiceMock = new Mock(); + IEnumerable ancestorsKeys = []; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(key, out ancestorsKeys)).Returns(true); + + var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings(), navigationQueryService: navigationQueryServiceMock.Object); var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor()); var result = builder.Build(content.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index c0001bf067..9da66ba10c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; @@ -26,7 +27,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.IsNotNull(result); Assert.AreEqual("/", result.Path); @@ -47,13 +48,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); IEnumerable ancestorsKeys = [rootKey]; - navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -77,14 +78,14 @@ public class ContentRouteBuilderTests : DeliveryApiTests var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); - Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), grandchild.Key)).Returns(grandchild); IEnumerable ancestorsKeys = [childKey, rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(grandchild); Assert.IsNotNull(result); Assert.AreEqual("/the-child/the-grandchild", result.Path); @@ -104,13 +105,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -136,13 +137,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -168,13 +169,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -197,7 +198,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var content = new Mock(); content.SetupGet(c => c.ItemType).Returns(itemType); - var builder = CreateApiContentRouteBuilder(true); + var builder = CreateApiContentRouteBuilder(true, Mock.Of()); Assert.Throws(() => builder.Build(content.Object)); } @@ -236,9 +237,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); var contentCache = Mock.Of(); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); - Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), grandchild.Key)).Returns(grandchild); IEnumerable grandchildAncestorsKeys = [childKey, rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out grandchildAncestorsKeys)).Returns(true); @@ -262,17 +263,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: true); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path); @@ -289,17 +290,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, addTrailingSlash, contentCache: contentCache, isPreview: true); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual(addTrailingSlash, result.Path.EndsWith("/")); @@ -314,9 +315,6 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock, published: false); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); @@ -324,13 +322,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: isPreview); var result = builder.Build(child); if (isPreview) @@ -358,8 +356,8 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); var apiContentPathProvider = new Mock(); apiContentPathProvider @@ -369,7 +367,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object); var result = builder.Build(root); Assert.NotNull(result); Assert.AreEqual("/my-custom-path-for-the-root", result.Path); @@ -432,7 +430,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests string Url(IPublishedContent content, string? culture) { - var ancestorsOrSelf = content.AncestorsOrSelf(variantContextAccessor, contentCache, navigationQueryService, PublishStatusQueryService).ToArray(); + var publishedContentStatusFilteringService = new PublishedContentStatusFilteringService( + variantContextAccessor, + PublishStatusQueryService, + Mock.Of(), + contentCache); + var ancestorsOrSelf = content.AncestorsOrSelf(navigationQueryService, publishedContentStatusFilteringService).ToArray(); return ancestorsOrSelf.All(c => c.IsPublished(culture)) ? string.Join("/", ancestorsOrSelf.Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/") : "#"; @@ -448,7 +451,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService) => new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService)); - private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null, IDocumentNavigationQueryService navigationQueryService = null) + private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, IDocumentNavigationQueryService navigationQueryService, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null) { var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash }; var requestHandlerSettingsMonitorMock = new Mock>(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 2764dd21d3..08c09c92f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -113,13 +113,14 @@ public class DeliveryApiTests content.SetupGet(c => c.ContentType).Returns(contentType); content.SetupGet(c => c.Properties).Returns(properties); content.SetupGet(c => c.ItemType).Returns(contentType.ItemType); + content.SetupGet(c => c.Level).Returns(1); content.Setup(c => c.IsPublished(It.IsAny())).Returns(true); } protected string DefaultUrlSegment(string name, string? culture = null) => $"{name.ToLowerInvariant().Replace(" ", "-")}{(culture.IsNullOrWhiteSpace() ? string.Empty : $"-{culture}")}"; - protected ApiContentRouteBuilder CreateContentRouteBuilder( + protected virtual ApiContentRouteBuilder CreateContentRouteBuilder( IApiContentPathProvider contentPathProvider, IOptions globalSettings, IVariationContextAccessor? variationContextAccessor = null, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index fc232d6c95..4b6c052392 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -21,7 +21,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var contentNameProvider = new ApiContentNameProvider(); var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider); - routeBuilder = routeBuilder ?? CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); + routeBuilder ??= CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiNodeTreePickerValueConverter( Mock.Of(), Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs index 7f952c35da..2751cf6f13 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs @@ -1,9 +1,12 @@ +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -31,6 +34,8 @@ public class PropertyValueConverterTests : DeliveryApiTests protected VariationContext VariationContext { get; } = new(); + protected Mock DocumentNavigationQueryServiceMock { get; private set; } + [SetUp] public override void Setup() { @@ -76,6 +81,10 @@ public class PropertyValueConverterTests : DeliveryApiTests .Returns("the-media-url"); PublishedUrlProvider = PublishedUrlProviderMock.Object; ApiContentPathProvider = new ApiContentPathProvider(PublishedUrlProvider); + + DocumentNavigationQueryServiceMock = new Mock(); + IEnumerable ancestorsKeys = []; + DocumentNavigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(contentKey, out ancestorsKeys)).Returns(true); } protected Mock SetupPublishedContent(string name, Guid key, PublishedItemType itemType, IPublishedContentType contentType) @@ -109,4 +118,28 @@ public class PropertyValueConverterTests : DeliveryApiTests .Setup(pcc => pcc.GetById(It.IsAny(), media.Key)) .Returns(media); } + + protected override ApiContentRouteBuilder CreateContentRouteBuilder( + IApiContentPathProvider contentPathProvider, + IOptions globalSettings, + IVariationContextAccessor? variationContextAccessor = null, + IRequestPreviewService? requestPreviewService = null, + IOptionsMonitor? requestHandlerSettingsMonitor = null, + IPublishedContentCache? contentCache = null, + IDocumentNavigationQueryService? navigationQueryService = null, + IPublishStatusQueryService? publishStatusQueryService = null) + { + contentCache ??= PublishedContentCacheMock.Object; + navigationQueryService ??= DocumentNavigationQueryServiceMock.Object; + + return base.CreateContentRouteBuilder( + contentPathProvider, + globalSettings, + variationContextAccessor, + requestPreviewService, + requestHandlerSettingsMonitor, + contentCache, + navigationQueryService, + publishStatusQueryService); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index 931f39d8fe..33f4f307f8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Serialization; @@ -37,7 +38,9 @@ public class DataValueReferenceFactoryCollectionTests Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of())); + Mock.Of(), + Mock.Of(), + Mock.Of())); private IIOHelper IOHelper { get; } = Mock.Of(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs index 18254d923e..59569c7583 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs @@ -1,9 +1,11 @@ -using Moq; +using System.Globalization; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -71,6 +73,84 @@ public class DecimalValueEditorTests Assert.IsNull(result); } + [TestCase("x", false)] + [TestCase(1.5, true)] + public void Validates_Is_Decimal(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, $"The value {value} is not a valid decimal"); + } + } + + [TestCase(0.9, false)] + [TestCase(1.1, true)] + [TestCase(1.3, true)] + public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMinimum"); + } + } + + [TestCase(1.7, true)] + [TestCase(1.9, true)] + [TestCase(2.1, false)] + public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + } + } + + [TestCase(1.4, false)] + [TestCase(1.5, true)] + public void Validates_Matches_Configured_Step(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidStep"); + } + } + private static object? FromEditor(object? value) => CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null); @@ -86,11 +166,26 @@ public class DecimalValueEditorTests private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor() { - var valueEditor = new DecimalPropertyEditor.DecimalPropertyValueEditor( + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new DecimalPropertyEditor.DecimalPropertyValueEditor( Mock.Of(), Mock.Of(), Mock.Of(), - new DataEditorAttribute("alias")); - return valueEditor; + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new Dictionary + { + { "min", 1.1 }, + { "max", 1.9 }, + { "step", 0.2 } + } + }; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs new file mode 100644 index 0000000000..d9545b8126 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs @@ -0,0 +1,191 @@ +using System.Globalization; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +public class IntegerValueEditorTests +{ + // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( + private Dictionary _valuesAndExpectedResults = new(); + + [SetUp] + public void SetUp() => _valuesAndExpectedResults = new Dictionary + { + { 123m, 123 }, + { 123, 123 }, + { -123, -123 }, + { 123.45d, null }, + { "123.45", null }, + { "1234.56", null }, + { "123,45", null }, + { "1.234,56", null }, + { "123 45", null }, + { "something", null }, + { true, null }, + { new object(), null }, + { new List { "some", "values" }, null }, + { Guid.NewGuid(), null }, + { new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()), null } + }; + + [Test] + public void Can_Parse_Values_From_Editor() + { + foreach (var (value, expected) in _valuesAndExpectedResults) + { + var fromEditor = FromEditor(value); + Assert.AreEqual(expected, fromEditor, message: $"Failed for: {value}"); + } + } + + [Test] + public void Can_Parse_Values_To_Editor() + { + foreach (var (value, expected) in _valuesAndExpectedResults) + { + var toEditor = ToEditor(value); + Assert.AreEqual(expected, toEditor, message: $"Failed for: {value}"); + } + } + + [Test] + public void Null_From_Editor_Yields_Null() + { + var result = FromEditor(null); + Assert.IsNull(result); + } + + [Test] + public void Null_To_Editor_Yields_Null() + { + var result = ToEditor(null); + Assert.IsNull(result); + } + + [TestCase("x", false)] + [TestCase(10, true)] + public void Validates_Is_Integer(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual($"The value {value} is not a valid integer", validationResult.ErrorMessage); + } + } + + [TestCase(8, false)] + [TestCase(10, true)] + [TestCase(12, true)] + public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_outOfRangeMinimum", validationResult.ErrorMessage); + } + } + + [TestCase(18, true)] + [TestCase(20, true)] + [TestCase(22, false)] + public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_outOfRangeMaximum", validationResult.ErrorMessage); + } + } + + [TestCase(17, false)] + [TestCase(18, true)] + public void Validates_Matches_Configured_Step(object value, bool expectedSuccess) + { + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual("validation_invalidStep", validationResult.ErrorMessage); + } + } + + private static object? FromEditor(object? value) + => CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null); + + private static object? ToEditor(object? value) + { + var property = new Mock(); + property + .Setup(p => p.GetValue(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(value); + + return CreateValueEditor().ToEditor(property.Object); + } + + private static IntegerPropertyEditor.IntegerPropertyValueEditor CreateValueEditor() + { + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new IntegerPropertyEditor.IntegerPropertyValueEditor( + Mock.Of(), + Mock.Of(), + Mock.Of(), + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new Dictionary + { + { "min", 10 }, + { "max", 20 }, + { "step", 2 } + } + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs new file mode 100644 index 0000000000..4f6ee99ad4 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs @@ -0,0 +1,215 @@ +using System.ComponentModel.DataAnnotations; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Validation; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; + +[TestFixture] +internal class MediaPicker3ValueEditorValidationTests +{ + [TestCase(true, true)] + [TestCase(false, false)] + public void Validates_Start_Node_Immediate_Parent(bool shouldSucceed, bool hasValidParentKey) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + Guid? validParentKey = Guid.NewGuid(); + var mediaKey = Guid.NewGuid(); + + if (hasValidParentKey) + { + mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out validParentKey)).Returns(true); + } + else + { + Guid? invalidParentKey = Guid.NewGuid(); + mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out invalidParentKey)).Returns(true); + } + + valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = validParentKey }; + + var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(shouldSucceed, result); + } + + [Test] + public void Validates_Start_Node_Parent_Not_Found() + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + Guid? parentKey = null; + var mediaKey = Guid.NewGuid(); + + mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out parentKey)).Returns(true); + + valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = Guid.NewGuid() }; + + var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(false, result); + } + + [Test] + [TestCase(true, true, true)] + [TestCase(false, false, true)] + [TestCase(false, true, false)] + public void Validates_Start_Node_Ancestor(bool shouldSucceed, bool findsAncestor, bool hasValidAncestorKey) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + Guid ancestorKey = Guid.NewGuid(); + Guid? parentKey = Guid.NewGuid(); + var mediaKey = Guid.NewGuid(); + IEnumerable ancestorKeys = findsAncestor is false ? [] : hasValidAncestorKey ? [ancestorKey] : [Guid.NewGuid()]; + + mediaNavigationQueryServiceMock.Setup(x => x.TryGetParentKey(mediaKey, out parentKey)).Returns(true); + mediaNavigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(parentKey.Value, out ancestorKeys)).Returns(true); + + valueEditor.ConfigurationObject = new MediaPicker3Configuration { StartNodeId = ancestorKey }; + + var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(shouldSucceed, result); + } + + [TestCase(true, true, true)] + [TestCase(false, true, false)] + [TestCase(false, false, true)] + public void Validates_Allowed_Type(bool shouldSucceed, bool hasAllowedType, bool findsMediaType) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + var mediaKey = Guid.NewGuid(); + var mediaTypeKey = Guid.NewGuid(); + var mediaTypeAlias = "Alias"; + valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Filter = $"{mediaTypeKey}" }; + var mediaTypeMock = new Mock(); + + if (hasAllowedType) + { + mediaTypeMock.Setup(x => x.Key).Returns(mediaTypeKey); + } + else + { + mediaTypeMock.Setup(x => x.Key).Returns(Guid.NewGuid()); + } + + if (findsMediaType) + { + mediaTypeServiceMock.Setup(x => x.Get(mediaTypeAlias)).Returns(mediaTypeMock.Object); + } + else + { + mediaTypeServiceMock.Setup(x => x.Get(It.IsAny())).Returns((IMediaType)null); + } + + var value = "[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"" + mediaKey + "\",\n \"mediaTypeAlias\" : \"" + mediaTypeAlias + "\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]"; + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(shouldSucceed, result); + } + + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", false, true)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", false, false)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", true, true)] + [TestCase("[]", true, true)] + [TestCase("[]", false, true)] + public void Validates_Multiple(string value, bool multiple, bool succeed) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = multiple }; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + ValidateResult(succeed, result); + } + + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, false)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, true)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, true)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 3, false)] + [TestCase("[]", 1, false)] + [TestCase("[]", 0, true)] + public void Validates_Min_Limit(string value, int min, bool succeed) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Min = min } }; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + + ValidateResult(succeed, result); + } + + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, true)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 0, false)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 1, false)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 2, true)] + [TestCase("[ {\n \" key\" : \"20266ebe-1f7e-4cf3-a694-7a5fb210223b\",\n \"mediaKey\" : \"7AD39018-0920-4818-89D3-26F47DBCE62E\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n}, {\n \" key\" : \"1C70519E-C3AE-4D45-8E48-30B3D02E455E\",\n \"mediaKey\" : \"E243A7E2-8D2E-4DC9-88FB-822350A40142\",\n \"mediaTypeAlias\" : \"\",\n \"crops\" : [ ],\n \"focalPoint\" : null\n} ]", 3, true)] + [TestCase("[]", 1, true)] + [TestCase("[]", 0, true)] + public void Validates_Max_Limit(string value, int max, bool succeed) + { + var (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock) = CreateValueEditor(); + + valueEditor.ConfigurationObject = new MediaPicker3Configuration() { Multiple = true, ValidationLimit = new MediaPicker3Configuration.NumberRange { Max = max } }; + + var result = valueEditor.Validate(value, false, null, PropertyValidationContext.Empty()); + ValidateResult(succeed, result); + } + + private static void ValidateResult(bool succeed, IEnumerable result) + { + if (succeed) + { + Assert.IsEmpty(result); + } + else + { + Assert.That(result.Count(), Is.EqualTo(1)); + } + } + + private static (MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor ValueEditor, Mock MediaTypeServiceMock, Mock MediaNavigationQueryServiceMock) CreateValueEditor() + { + var mediaTypeServiceMock = new Mock(); + var mediaNavigationQueryServiceMock = new Mock(); + var valueEditor = new MediaPicker3PropertyEditor.MediaPicker3PropertyValueEditor( + Mock.Of(), + new SystemTextJsonSerializer(), + Mock.Of(), + new DataEditorAttribute("alias"), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + mediaTypeServiceMock.Object, + mediaNavigationQueryServiceMock.Object) + { + ConfigurationObject = new MediaPicker3Configuration() + }; + + return (valueEditor, mediaTypeServiceMock, mediaNavigationQueryServiceMock); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NumericValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NumericValueEditorTests.cs deleted file mode 100644 index 683be6dce2..0000000000 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NumericValueEditorTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Editors; -using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; - -[TestFixture] -public class NumericValueEditorTests -{ - // annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :( - private Dictionary _valuesAndExpectedResults = new(); - - [SetUp] - public void SetUp() => _valuesAndExpectedResults = new Dictionary - { - { 123m, 123 }, - { 123, 123 }, - { -123, -123 }, - { 123.45d, null }, - { "123.45", null }, - { "1234.56", null }, - { "123,45", null }, - { "1.234,56", null }, - { "123 45", null }, - { "something", null }, - { true, null }, - { new object(), null }, - { new List { "some", "values" }, null }, - { Guid.NewGuid(), null }, - { new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()), null } - }; - - [Test] - public void Can_Parse_Values_From_Editor() - { - foreach (var (value, expected) in _valuesAndExpectedResults) - { - var fromEditor = FromEditor(value); - Assert.AreEqual(expected, fromEditor, message: $"Failed for: {value}"); - } - } - - [Test] - public void Can_Parse_Values_To_Editor() - { - foreach (var (value, expected) in _valuesAndExpectedResults) - { - var toEditor = ToEditor(value); - Assert.AreEqual(expected, toEditor, message: $"Failed for: {value}"); - } - } - - [Test] - public void Null_From_Editor_Yields_Null() - { - var result = FromEditor(null); - Assert.IsNull(result); - } - - [Test] - public void Null_To_Editor_Yields_Null() - { - var result = ToEditor(null); - Assert.IsNull(result); - } - - private static object? FromEditor(object? value) - => CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null); - - private static object? ToEditor(object? value) - { - var property = new Mock(); - property - .Setup(p => p.GetValue(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(value); - - return CreateValueEditor().ToEditor(property.Object); - } - - private static IntegerPropertyEditor.IntegerPropertyValueEditor CreateValueEditor() - { - var valueEditor = new IntegerPropertyEditor.IntegerPropertyValueEditor( - Mock.Of(), - Mock.Of(), - Mock.Of(), - new DataEditorAttribute("alias")); - return valueEditor; - } -} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs index 6ec212c02e..7adda7a525 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/SliderValueEditorTests.cs @@ -1,11 +1,15 @@ -using System.Text.Json.Nodes; +using System.Globalization; +using System.Text.Json.Nodes; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Models.Validation; using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; @@ -14,7 +18,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] public class SliderValueEditorTests { +#pragma warning disable IDE1006 // Naming Styles public static object[] InvalidCaseData = new object[] +#pragma warning restore IDE1006 // Naming Styles { 123m, 123, @@ -99,6 +105,132 @@ public class SliderValueEditorTests Assert.IsNull(result); } + [TestCase(true, 1.1, 1.1, true)] + [TestCase(true, 1.1, 1.3, true)] + [TestCase(false, 1.1, 1.1, true)] + [TestCase(false, 1.1, 1.3, false)] + public void Validates_Contains_Range_Only_When_Enabled(bool enableRange, decimal from, decimal to, bool expectedSuccess) + { + var value = new JsonObject + { + { "from", from }, + { "to", to }, + }; + var editor = CreateValueEditor(enableRange: enableRange); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_unexpectedRange"); + } + } + + [TestCase(1.1, 1.1, true)] + [TestCase(1.1, 1.3, true)] + [TestCase(1.3, 1.1, false)] + public void Validates_Contains_Valid_Range_Only_When_Enabled(decimal from, decimal to, bool expectedSuccess) + { + var value = new JsonObject + { + { "from", from }, + { "to", to }, + }; + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidRange"); + } + } + + [TestCase(0.9, 1.1, false)] + [TestCase(1.1, 1.1, true)] + [TestCase(1.3, 1.7, true)] + public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(decimal from, decimal to, bool expectedSuccess) + { + var value = new JsonObject + { + { "from", from }, + { "to", to }, + }; + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMinimum"); + } + } + + [TestCase(1.3, 1.7, true)] + [TestCase(1.9, 1.9, true)] + [TestCase(1.9, 2.1, false)] + public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(decimal from, decimal to, bool expectedSuccess) + { + var value = new JsonObject + { + { "from", from }, + { "to", to }, + }; + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_outOfRangeMaximum"); + } + } + + [TestCase(1.3, 1.7, true)] + [TestCase(1.4, 1.7, false)] + [TestCase(1.3, 1.6, false)] + public void Validates_Matches_Configured_Step(decimal from, decimal to, bool expectedSuccess) + { + var value = new JsonObject + { + { "from", from }, + { "to", to }, + }; + var editor = CreateValueEditor(); + var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()); + if (expectedSuccess) + { + Assert.IsEmpty(result); + } + else + { + Assert.AreEqual(1, result.Count()); + + var validationResult = result.First(); + Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidStep"); + } + } + private static object? FromEditor(object? value) => CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null); @@ -112,13 +244,29 @@ public class SliderValueEditorTests return CreateValueEditor().ToEditor(property.Object); } - private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor() + private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor(bool enableRange = true) { - var valueEditor = new SliderPropertyEditor.SliderPropertyValueEditor( + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}"); + return new SliderPropertyEditor.SliderPropertyValueEditor( Mock.Of(), new SystemTextJsonSerializer(), Mock.Of(), - new DataEditorAttribute("alias")); - return valueEditor; + new DataEditorAttribute("alias"), + localizedTextServiceMock.Object) + { + ConfigurationObject = new SliderConfiguration + { + EnableRange = enableRange, + MinimumValue = 1.1m, + MaximumValue = 1.9m, + Step = 0.2m + }, + }; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs index d7f86ff11e..11872abbfb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using AutoFixture.NUnit3; +using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -30,16 +31,14 @@ public class ContentFinderByUrlAliasTests [Frozen] IPublishedContentCache publishedContentCache, [Frozen] IUmbracoContextAccessor umbracoContextAccessor, [Frozen] IUmbracoContext umbracoContext, - [Frozen] IVariationContextAccessor variationContextAccessor, - [Frozen] IPublishStatusQueryService publishStatusQueryService, + [Frozen] IDocumentNavigationQueryService documentNavigationQueryService, + [Frozen] IPublishedContentStatusFilteringService publishedContentStatusFilteringService, IFileService fileService, - ContentFinderByUrlAlias sut, IPublishedContent[] rootContents, IPublishedProperty urlProperty) { // Arrange var absoluteUrl = "http://localhost" + relativeUrl; - var variationContext = new VariationContext(); var contentItem = rootContents[0]; Mock.Get(umbracoContextAccessor).Setup(x => x.TryGetUmbracoContext(out umbracoContext)).Returns(true); @@ -47,13 +46,22 @@ public class ContentFinderByUrlAliasTests Mock.Get(publishedContentCache).Setup(x => x.GetAtRoot(null)).Returns(rootContents); Mock.Get(contentItem).Setup(x => x.Id).Returns(nodeMatch); Mock.Get(contentItem).Setup(x => x.GetProperty(Constants.Conventions.Content.UrlAlias)).Returns(urlProperty); + Mock.Get(contentItem).Setup(x => x.ItemType).Returns(PublishedItemType.Content); Mock.Get(urlProperty).Setup(x => x.GetValue(null, null)).Returns(relativeUrl); - Mock.Get(variationContextAccessor).Setup(x => x.VariationContext).Returns(variationContext); - Mock.Get(publishStatusQueryService).Setup(x => x.IsDocumentPublished(It.IsAny(), It.IsAny())).Returns(true); + IEnumerable descendantKeys = []; + Mock.Get(documentNavigationQueryService).Setup(x => x.TryGetDescendantsKeys(It.IsAny(), out descendantKeys)).Returns(true); + + Mock.Get(publishedContentStatusFilteringService).Setup(x => x.FilterAvailable(It.IsAny>(), It.IsAny())).Returns([]); var publishedRequestBuilder = new PublishedRequestBuilder(new Uri(absoluteUrl, UriKind.Absolute), fileService); // Act + var sut = new ContentFinderByUrlAlias( + Mock.Of>(), + Mock.Of(), + umbracoContextAccessor, + documentNavigationQueryService, + publishedContentStatusFilteringService); var result = await sut.TryFindContent(publishedRequestBuilder); Assert.IsTrue(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs new file mode 100644 index 0000000000..674c06d643 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs @@ -0,0 +1,357 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services.PublishStatus; + +[TestFixture] +public partial class PublishedContentStatusFilteringServiceTests +{ + [Test] + public void FilterAvailable_Invariant_ForNonPreview_YieldsPublishedItems() + { + var (sut, items) = SetupInvariant(false); + + var children = sut.FilterAvailable(items.Keys, null).ToArray(); + Assert.AreEqual(5, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + Assert.AreEqual(4, children[2].Id); + Assert.AreEqual(6, children[3].Id); + Assert.AreEqual(8, children[4].Id); + }); + } + + [Test] + public void FilterAvailable_Invariant_ForPreview_YieldsUnpublishedItems() + { + var (sut, items) = SetupInvariant(true); + + var children = sut.FilterAvailable(items.Keys, null).ToArray(); + Assert.AreEqual(10, children.Length); + for (var i = 0; i < 10; i++) + { + Assert.AreEqual(i, children[i].Id); + } + } + + [TestCase("da-DK", 3)] + [TestCase("en-US", 4)] + public void FilterAvailable_Variant_ForNonPreview_YieldsPublishedItemsInCulture(string culture, int expectedNumberOfChildren) + { + var (sut, items) = SetupVariant(false, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(expectedNumberOfChildren, children.Length); + + // IDs 0 through 3 exist in both en-US and da-DK - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + }); + + // IDs 4 through 6 exist only in en-US - only even IDs are published + if (culture == "en-US") + { + Assert.AreEqual(4, children[2].Id); + Assert.AreEqual(6, children[3].Id); + } + + // IDs 7 through 9 exist only in da-DK - only even IDs are published + if (culture == "da-DK") + { + Assert.AreEqual(8, children[2].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(string culture) + { + var (sut, items) = SetupVariant(true, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(7, children.Length); + + // IDs 0 through 3 exist in both en-US and da-DK + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(1, children[1].Id); + Assert.AreEqual(2, children[2].Id); + Assert.AreEqual(3, children[3].Id); + }); + + // IDs 4 through 6 exist only in en-US + if (culture == "en-US") + { + Assert.AreEqual(4, children[4].Id); + Assert.AreEqual(5, children[5].Id); + Assert.AreEqual(6, children[6].Id); + } + + // IDs 7 through 9 exist only in da-DK + if (culture == "da-DK") + { + Assert.AreEqual(7, children[4].Id); + Assert.AreEqual(8, children[5].Id); + Assert.AreEqual(9, children[6].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + { + var (sut, items) = SetupMixedVariance(false, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(4, children.Length); + + // IDs 0 through 2 are invariant - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + }); + + // IDs 3 through 5 exist in both en-US and da-DK - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(4, children[2].Id); + }); + + // IDs 6 and 7 exist only in en-US - only even IDs are published + if (culture == "en-US") + { + Assert.AreEqual(6, children[3].Id); + } + + // IDs 8 and 9 exist only in da-DK - only even IDs are published + if (culture == "da-DK") + { + Assert.AreEqual(8, children[3].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_MixedVariance_FoPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + { + var (sut, items) = SetupMixedVariance(true, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(8, children.Length); + + // IDs 0 through 2 are invariant + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(1, children[1].Id); + Assert.AreEqual(2, children[2].Id); + }); + + // IDs 3 through 5 exist in both en-US and da-DK + Assert.Multiple(() => + { + Assert.AreEqual(3, children[3].Id); + Assert.AreEqual(4, children[4].Id); + Assert.AreEqual(5, children[5].Id); + }); + + // IDs 6 and 7 exist only in en-US + if (culture == "en-US") + { + Assert.AreEqual(6, children[6].Id); + Assert.AreEqual(7, children[7].Id); + } + + // IDs 8 and 9 exist only in da-DK + if (culture == "da-DK") + { + Assert.AreEqual(8, children[6].Id); + Assert.AreEqual(9, children[7].Id); + } + } + + // sets up invariant test data: + // - 10 documents with IDs 0 through 9 + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupInvariant(bool forPreview) + { + var contentType = new Mock(); + contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + + var key = Guid.NewGuid(); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(new Dictionary()); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(null); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + // sets up variant test data: + // - 10 documents with IDs 0 through 9 + // - IDs 0 through 3 exist in both en-US and da-DK + // - IDs 4 through 6 exist only in en-US + // - IDs 7 through 9 exist only in da-DK + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupVariant(bool forPreview, string requestCulture) + { + var contentType = new Mock(); + contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + + var key = Guid.NewGuid(); + string[] cultures = i <= 3 + ? ["da-DK", "en-US"] + : i <= 6 + ? ["en-US"] + : ["da-DK"]; + var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue)); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(cultureDictionary); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(requestCulture); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + // sets up mixed variant test data: + // - 10 documents with IDs 0 through 9 + // - IDs 0 through 2 are invariant + // - IDs 3 through 5 exist in both en-US and da-DK + // - IDs 6 and 7 exist only in en-US + // - IDs 8 and 9 exist only in da-DK + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupMixedVariance(bool forPreview, string requestCulture) + { + var invariantContentType = new Mock(); + invariantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + + var variantContentType = new Mock(); + variantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + var contentType = i <= 2 + ? invariantContentType + : variantContentType; + + var key = Guid.NewGuid(); + string[] cultures = i <= 2 + ? [] + : i <= 5 + ? ["da-DK", "en-US"] + : i <= 7 + ? ["en-US"] + : ["da-DK"]; + var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue)); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(cultureDictionary); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(requestCulture); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary items) + => SetupPublishStatusQueryService(items, id => id % 2 == 0); + + private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary items, Func idIsPublished) + { + var publishStatusQueryService = new Mock(); + publishStatusQueryService + .Setup(p => p.IsDocumentPublished(It.IsAny(), It.IsAny())) + .Returns((Guid key, string culture) => items + .TryGetValue(key, out var item) + && idIsPublished(item.Id) + && (item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture))); + return publishStatusQueryService.Object; + } + + private IPreviewService SetupPreviewService(bool forPreview) + { + var previewService = new Mock(); + previewService.Setup(p => p.IsInPreview()).Returns(forPreview); + return previewService.Object; + } + + private IVariationContextAccessor SetupVariantContextAccessor(string? requestCulture) + { + var variationContextAccessor = new Mock(); + variationContextAccessor.SetupGet(v => v.VariationContext).Returns(new VariationContext(requestCulture)); + return variationContextAccessor.Object; + } + + private IPublishedContentCache SetupPublishedContentCache(bool forPreview, Dictionary items) + { + var publishedContentCache = new Mock(); + publishedContentCache + .Setup(c => c.GetById(forPreview, It.IsAny())) + .Returns((bool preview, Guid key) => items.TryGetValue(key, out var item) ? item : null); + return publishedContentCache.Object; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs index 1a4a73f800..399633da2a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs @@ -98,9 +98,8 @@ public class HtmlImageSourceParserTests new UrlProviderCollection(() => Enumerable.Empty()), new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of()); using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index ad2fb0b231..d1e5e0f494 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -222,8 +222,12 @@ public class HtmlLocalLinkParserTests var webRoutingSettings = new WebRoutingSettings(); var navigationQueryService = new Mock(); - Guid? parentKey = null; - navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); + // Guid? parentKey = null; + // navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); + IEnumerable ancestorKeys = []; + navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny(), out ancestorKeys)).Returns(true); + + var publishedContentStatusFilteringService = new Mock(); using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { @@ -246,9 +250,8 @@ public class HtmlLocalLinkParserTests new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), Mock.Of(), - contentCache.Object, navigationQueryService.Object, - publishStatusQueryService.Object); + publishedContentStatusFilteringService.Object); var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 8661e94e37..0a1c88c5b8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -3,7 +3,7 @@ true Umbraco.Cms.Tests.UnitTests - + $(WarningsNotAsErrors),SYSLIB0013,CS0618,CS1998,SA1117,CS0067,CA1822,CA1416,IDE0028,SA1401,SA1405,IDE0060,ASP0019,CS0114,CS0661,CS0659,CS0414,CS0252,CS0612,IDE1006 - +