From bb946a578d413bf3747f63f8f5406f11440cc009 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 29 Nov 2024 07:20:02 +1100 Subject: [PATCH 01/38] MetaData Config & Render calls configured --- Sitecore.AspNetCore.SDK.sln | 7 + .../Configuration/PagesMarkerService.cs | 6 + .../Configuration/PagesOptions.cs | 32 ++++ .../PagesAppConfigurationExtensions.cs | 58 ++++++++ .../Middleware/PagesConfigMiddleware.cs | 138 ++++++++++++++++++ .../Middleware/PagesRenderMiddleware.cs | 96 ++++++++++++ .../Models/PagesRenderArgs.cs | 50 +++++++ .../Sitecore.AspNetCore.SDK.Pages.csproj | 13 ++ 8 files changed, 400 insertions(+) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesMarkerService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj diff --git a/Sitecore.AspNetCore.SDK.sln b/Sitecore.AspNetCore.SDK.sln index fd5cc1b..7fc2785 100644 --- a/Sitecore.AspNetCore.SDK.sln +++ b/Sitecore.AspNetCore.SDK.sln @@ -112,6 +112,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM .github\ISSUE_TEMPLATE\Question.yml = .github\ISSUE_TEMPLATE\Question.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sitecore.AspNetCore.SDK.Pages", "src\Sitecore.AspNetCore.SDK.Pages\Sitecore.AspNetCore.SDK.Pages.csproj", "{F33DC6F7-83F8-41CE-852C-8279D1266139}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -186,6 +188,10 @@ Global {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {100C07C6-C68D-469F-9F15-139CB48CB7F0}.Release|Any CPU.Build.0 = Release|Any CPU + {F33DC6F7-83F8-41CE-852C-8279D1266139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F33DC6F7-83F8-41CE-852C-8279D1266139}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F33DC6F7-83F8-41CE-852C-8279D1266139}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F33DC6F7-83F8-41CE-852C-8279D1266139}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -221,6 +227,7 @@ Global {100C07C6-C68D-469F-9F15-139CB48CB7F0} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} {1706E43D-AC19-4FBB-9BFB-18A8B195580A} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} {24CCC156-046B-4600-9DB0-FC3269A18747} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} + {F33DC6F7-83F8-41CE-852C-8279D1266139} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E4F7126-B772-42CB-8F90-93B221ED0A72} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesMarkerService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesMarkerService.cs new file mode 100644 index 0000000..81564ff --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesMarkerService.cs @@ -0,0 +1,6 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Configuration; + +/// +/// Marker service used to identify when Pages services have been registered. +/// +internal class PagesMarkerService; \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs new file mode 100644 index 0000000..8cf82c0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -0,0 +1,32 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Configuration; + +/// +/// The options to configure the Pages middleware. +/// +public class PagesOptions +{ + /// + /// Gets or sets the config endpoint for Pages MetaData mode. + /// + public string ConfigEndpoint { get; set; } = "/api/editing/config"; + + /// + /// Gets or sets the render endpoint for Pages MetaData mode. + /// + public string RenderEndpoint { get; set; } = "/api/editing/render"; + + /// + /// Gets or sets the valid editing origin for all editing requests. + /// + public string ValidEditingOrigin { get; set; } = "https://pages.sitecorecloud.io"; + + /// + /// Gets or sets the valid origins for the head to run under. + /// + public string ValidOrigins { get; set; } = string.Empty; + + /// + /// Gets or sets the Editing Secret. + /// + public string EditingSecret { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs new file mode 100644 index 0000000..e187144 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.Pages.Extensions; + +/// +/// Configuration helpers for Pages functionality. +/// +public static class PagesAppConfigurationExtensions +{ + /// + /// Registers the Sitecore Experience Editor middleware into the . + /// + /// The instance of the to extend. + /// The so that additional calls can be chained. + public static IApplicationBuilder UseSitecorePages(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + object? experienceEditorMarker = app.ApplicationServices.GetService(typeof(PagesMarkerService)); + if (experienceEditorMarker != null) + { + app.UseMiddleware(); + app.UseMiddleware(); + } + + return app; + } + + /// + /// Adds the Sitecore Experience Editor support services to the . + /// + /// The to add services to. + /// Configures the options. + /// The so that additional calls can be chained. + public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRenderingEngineBuilder serviceBuilder, Action? options = null) + { + ArgumentNullException.ThrowIfNull(serviceBuilder); + + IServiceCollection services = serviceBuilder.Services; + if (services.Any(s => s.ServiceType == typeof(PagesMarkerService))) + { + return serviceBuilder; + } + + services.AddSingleton(); + + if (options != null) + { + services.Configure(options); + } + + return serviceBuilder; + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs new file mode 100644 index 0000000..d2779cb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs @@ -0,0 +1,138 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +namespace Sitecore.AspNetCore.SDK.Pages.Middleware; + +/// +/// The Pages middleware implementation that handles GET requests from the Sitecore Pages in MetaData Editing mode +/// and wraps the response HTML in a JSON format. +/// +/// +/// Initializes a new instance of the class. +/// +/// The next middleware to call. +/// The Sitecore Pages configuration options. +/// The to use for logging. +/// The RenderingEngineOptions, used to retriece a list of all registered renderings for the applications +public class PagesConfigMiddleware(RequestDelegate next, IOptions options, ILogger logger, IOptions renderingEngineOptions) +{ + private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); + private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly RenderingEngineOptions renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); + + /// + /// The middleware Invoke method. + /// + /// The current . + /// A Task to support async calls. + public async Task Invoke(HttpContext httpContext) + { + if (IsValidPagesConfigRequest(httpContext.Request)) + { + logger.LogDebug("Processing valid Pages Config request"); + await BuildResponse(httpContext.Response); + return; + } + + await next(httpContext).ConfigureAwait(false); + } + + private async Task BuildResponse(HttpResponse httpResponse) + { + httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; + httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; + httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; + + httpResponse.StatusCode = StatusCodes.Status200OK; + httpResponse.ContentType = "application/json"; + + // TODO: change the Components list to not be hardcoded! + string responseBody = @" + { + ""components"": [ + ""Title"", + ""Container"", + ""ColumnSplitter"", + ""RowSplitter"", + ""PageContent"", + ""RichText"", + ""Promo"", + ""LinkList"", + ""Image"", + ""PartialDesignDynamicPlaceholder"", + ""Navigation"" + ], + ""packages"": { + ""@sitecore/byoc"": ""0.2.15"", + ""@sitecore/components"": ""1.1.10"", + ""@sitecore/engage"": ""1.4.3"", + ""@sitecore-cloudsdk/core"": ""0.3.1"", + ""@sitecore-cloudsdk/events"": ""0.3.1"", + ""@sitecore-cloudsdk/personalize"": ""0.3.1"", + ""@sitecore-cloudsdk/utils"": ""0.3.1"", + ""@sitecore-feaas/clientside"": ""0.5.18"", + ""@sitecore-jss/sitecore-jss"": ""22.1.3"", + ""@sitecore-jss/sitecore-jss-cli"": ""22.1.3"", + ""@sitecore-jss/sitecore-jss-dev-tools"": ""22.1.3"", + ""@sitecore-jss/sitecore-jss-nextjs"": ""22.1.3"", + ""@sitecore-jss/sitecore-jss-react"": ""22.1.3"" + }, + ""editMode"": ""metadata"" + } + "; + + await httpResponse.WriteAsync(responseBody); + } + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == options.EditingSecret) + { + return true; + } + } + + return false; + } + + private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) + { + if (httpRequest.Headers.Origin == options.ValidEditingOrigin) + { + return true; + } + + return false; + } + + private bool IsValidPagesConfigRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.ConfigEndpoint, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (!IsValidEditingSecret(httpRequest)) + { + logger.LogError("Invalid Pages Editing Secret Value"); + return false; + } + + if (!RequestHasValidEditingOrigin(httpRequest)) + { + logger.LogError("Invalid Pages Editing Origin"); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs new file mode 100644 index 0000000..372ed35 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -0,0 +1,96 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Models; + +namespace Sitecore.AspNetCore.SDK.Pages.Middleware; + +/// +/// The Pages middleware implementation that handles GET requests from the Sitecore Pages in MetaData Editing mode +/// and wraps the response HTML in a JSON format. +/// +/// +/// Initializes a new instance of the class. +/// +/// The next middleware to call. +/// The Sitecore Pages configuration options. +/// The to use for logging. +public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ILogger logger) +{ + private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); + private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// The middleware Invoke method. + /// + /// The current . + /// A Task to support async calls. + public async Task Invoke(HttpContext httpContext) + { + if (IsValidPagesRenderRequest(httpContext.Request)) + { + logger.LogDebug("Processing valid Pages Render request"); + + PerformPagesRedirect(httpContext, ParseQueryStringArgs(httpContext.Request)); + + return; + } + + await next(httpContext).ConfigureAwait(false); + } + + private void PerformPagesRedirect(HttpContext httpContext, PagesRenderArgs args) + { + httpContext.Response.Redirect(args.Route, permanent: false); + } + + private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) + { + return new PagesRenderArgs + { + ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, + EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, + Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, + LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, + Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, + Route = request.Query["route"].FirstOrDefault() ?? string.Empty, + Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, + Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0 + }; + } + + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == options.EditingSecret) + { + return true; + } + } + + return false; + } + + private bool IsValidPagesRenderRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.RenderEndpoint, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (!IsValidEditingSecret(httpRequest)) + { + logger.LogError("Invalid Pages Editing Secret Value"); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs new file mode 100644 index 0000000..03a9bcf --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs @@ -0,0 +1,50 @@ +using GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Models +{ + /// + /// The model used to store the args passed in to the Render route when using Pages MetaData Editing mode. + /// + public class PagesRenderArgs + { + /// + /// Gets or sets the Id of the item being edited. + /// + public Guid ItemId { get; set; } + + /// + /// Gets or sets the lanugage of the item being edited. + /// + public string Language { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the site being edited. + /// + public string Site { get; set; } = string.Empty; + + /// + /// Gets or sets the Version of the item being edited. + /// + public int Version { get; set; } = 1; + + /// + /// Gets or sets the Layout kind of item being edited. + /// + public string LayoutKind { get; set; } = string.Empty; + + /// + /// Gets or sets the mode that the redering is running. + /// + public string Mode { get; set; } = string.Empty; + + /// + /// Gets or sets the Editing Secret. + /// + public string EditingSecret { get; set; } = string.Empty; + + /// + /// Gets or sets the route to the item within the site. + /// + public string Route { get; set; } = string.Empty; + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj new file mode 100644 index 0000000..8484286 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + From 3c023f6f2864ffcc9678f6c0c93aa161d6382b8a Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 6 Dec 2024 13:05:36 +1100 Subject: [PATCH 02/38] Configured metadata render method to correctly render page using new EditingQuery --- .../GraphQL/EditingLayoutQueryResponse.cs | 12 ++ .../GraphQL/GraphQlLayoutServiceHandler.cs | 177 ++++++++++++++---- .../PagesAppConfigurationExtensions.cs | 30 +++ .../Middleware/PagesRenderMiddleware.cs | 2 +- 4 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs new file mode 100644 index 0000000..b50da86 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +/// +/// Layout Service GraphQL Response. +/// +public class EditingLayoutQueryResponse +{ + /// + /// Gets or sets Item for the Editing Layout Response. + /// + public ItemModel? Item { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs index a3368c8..9b915b9 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Diagnostics.Metrics; +using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; @@ -43,9 +44,116 @@ public async Task Request(SitecoreLayoutRequest request, } else { - GraphQLRequest layoutRequest = new() + content = IsEditingRequest(request) + ? await HandleEditingLayoutRequest(request, requestLanguage, errors, content).ConfigureAwait(false) + : await HandleLayoutRequest(request, requestLanguage, errors, content).ConfigureAwait(false); + } + + return new SitecoreLayoutResponse(request, errors) + { + Content = content, + Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) + }; + } + + private static bool IsEditingRequest(SitecoreLayoutRequest request) + { + if (!request.ContainsKey("sc_request_headers_key") || + request["sc_request_headers_key"] is not Dictionary headers || + !headers.ContainsKey("mode")) + { + return false; + } + + return headers["mode"].Contains("edit"); + } + + private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors, SitecoreLayoutResponseContent? content) + { + // TODO: Handle population of Dictionary for large size with extra GQL requests + GraphQLRequest layoutRequest = new() + { + Query = @" + query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $version: String, $after: String, $pageSize: Int = 10) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + ", + OperationName = "EditingQuery", + Variables = new { - Query = @" + itemId = GetRequestArgValue(request, "sc_itemid"), + language = requestLanguage, + siteName = request.SiteName(), + version = GetRequestArgValue(request, "sc_version"), + pageSize = 50, + after = string.Empty + } + }; + + GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Item); + } + + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library + string? json = response.Data?.Item?.Rendered.ToString(); + if (json == null) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = _serializer.Deserialize(json); + if (_logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + } + } + + if (response.Errors != null) + { + errors.AddRange( + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + } + + return content; + } + + private object GetRequestArgValue(SitecoreLayoutRequest request, string argName) + { + if (!request.ContainsKey("sc_request_headers_key") || + request["sc_request_headers_key"] is not Dictionary headers || + !headers.ContainsKey(argName)) + { + throw new ArgumentException($"Unable to parse arg:{argName} for Pages MetaData Render request."); + } + + return headers[argName].FirstOrDefault() ?? string.Empty; + } + + private async Task HandleLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors, SitecoreLayoutResponseContent? content) + { + GraphQLRequest layoutRequest = new() + { + Query = @" query LayoutQuery($path: String!, $language: String!, $site: String!) { layout(routePath: $path, language: $language, site: $site) { item { @@ -53,48 +161,43 @@ query LayoutQuery($path: String!, $language: String!, $site: String!) { } } }", - OperationName = "LayoutQuery", - Variables = new - { - path = request.Path(), - language = requestLanguage, - site = request.SiteName() - } - }; - - GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); - if (_logger.IsEnabled(LogLevel.Debug)) + OperationName = "LayoutQuery", + Variables = new { - _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Layout); + path = request.Path(), + language = requestLanguage, + site = request.SiteName() } + }; - // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library - string? json = response.Data?.Layout?.Item?.Rendered.ToString(); - if (json == null) - { - errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); - } - else - { - content = _serializer.Deserialize(json); - if (_logger.IsEnabled(LogLevel.Debug)) - { - object? formattedDeserializeObject = JsonSerializer.Deserialize(json); - _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); - } - } + GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Layout); + } - if (response.Errors != null) + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library + string? json = response.Data?.Layout?.Item?.Rendered.ToString(); + if (json == null) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = _serializer.Deserialize(json); + if (_logger.IsEnabled(LogLevel.Debug)) { - errors.AddRange( - response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); } } - return new SitecoreLayoutResponse(request, errors) + if (response.Errors != null) { - Content = content, - Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) - }; + errors.AddRange( + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + } + + return content; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index e187144..e18fd23 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -1,7 +1,11 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; namespace Sitecore.AspNetCore.SDK.Pages.Extensions; @@ -53,6 +57,32 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe services.Configure(options); } + services.Configure((Action)(renderingOptions => + { + renderingOptions.MapToRequest((httpRequest, layoutRequest) => + { + MapRequest(httpRequest, layoutRequest, "mode"); + MapRequest(httpRequest, layoutRequest, "sc_itemid"); + MapRequest(httpRequest, layoutRequest, "sc_version"); + }); + })); + return serviceBuilder; } + + private static void MapRequest(HttpRequest httpRequest, SitecoreLayoutRequest layoutRequest, string paramName) + { + if (httpRequest.Query == null || !httpRequest.Query.ContainsKey(paramName)) + { + return; + } + + string[]? modeQueryValue = httpRequest.Query[paramName]; + if (modeQueryValue == null) + { + return; + } + + layoutRequest.AddHeader(paramName, modeQueryValue); + } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 372ed35..4ac3076 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -44,7 +44,7 @@ public async Task Invoke(HttpContext httpContext) private void PerformPagesRedirect(HttpContext httpContext, PagesRenderArgs args) { - httpContext.Response.Redirect(args.Route, permanent: false); + httpContext.Response.Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}", permanent: false); } private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) From 5a1d70de0b3fbb2f9f2895f08e1c310ce4eea754 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 11 Dec 2024 15:26:51 +0000 Subject: [PATCH 03/38] Moved GQL logic out of LC.CLient and back into Pages logic. Introduced EditScripts TagHelper --- .../GraphQL/GraphQlLayoutServiceHandler.cs | 177 ++++-------------- .../Configuration/PagesOptions.cs | 10 +- .../Constants.cs | 30 +++ .../PagesAppConfigurationExtensions.cs | 38 +++- .../GraphQL/GraphQLClientFactory.cs | 34 ++++ .../GraphQL/IGraphQLClientFactory.cs | 26 +++ ...igMiddleware.cs => PageSetupMiddleware.cs} | 55 +++++- .../Middleware/PagesRenderMiddleware.cs | 108 ++++++----- .../Models/PagesRenderArgs.cs | 5 + .../GraphQL/EditingLayoutQueryResponse.cs | 4 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 156 +++++++++++++++ .../Response/CanvasState.cs | 51 +++++ .../Response/ClientData.cs | 21 +++ .../Response/EditingContext.cs | 22 +++ .../Sitecore.AspNetCore.SDK.Pages.csproj | 1 + .../TagHelpers/EditingScriptsTagHelper.cs | 74 ++++++++ 16 files changed, 609 insertions(+), 203 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Constants.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs rename src/Sitecore.AspNetCore.SDK.Pages/Middleware/{PagesConfigMiddleware.cs => PageSetupMiddleware.cs} (66%) rename src/{Sitecore.AspNetCore.SDK.LayoutService.Client => Sitecore.AspNetCore.SDK.Pages}/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs (62%) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Response/EditingContext.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs index 9b915b9..a3368c8 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.Metrics; -using System.Text.Json; +using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; @@ -44,116 +43,9 @@ public async Task Request(SitecoreLayoutRequest request, } else { - content = IsEditingRequest(request) - ? await HandleEditingLayoutRequest(request, requestLanguage, errors, content).ConfigureAwait(false) - : await HandleLayoutRequest(request, requestLanguage, errors, content).ConfigureAwait(false); - } - - return new SitecoreLayoutResponse(request, errors) - { - Content = content, - Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) - }; - } - - private static bool IsEditingRequest(SitecoreLayoutRequest request) - { - if (!request.ContainsKey("sc_request_headers_key") || - request["sc_request_headers_key"] is not Dictionary headers || - !headers.ContainsKey("mode")) - { - return false; - } - - return headers["mode"].Contains("edit"); - } - - private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors, SitecoreLayoutResponseContent? content) - { - // TODO: Handle population of Dictionary for large size with extra GQL requests - GraphQLRequest layoutRequest = new() - { - Query = @" - query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $version: String, $after: String, $pageSize: Int = 10) { - item(path: $itemId, language: $language, version: $version) { - rendered - } - site { - siteInfo(site: $siteName) { - dictionary(language: $language, first: $pageSize, after: $after) { - results { - key - value - } - pageInfo { - endCursor - hasNext - } - } - } - } - } - ", - OperationName = "EditingQuery", - Variables = new + GraphQLRequest layoutRequest = new() { - itemId = GetRequestArgValue(request, "sc_itemid"), - language = requestLanguage, - siteName = request.SiteName(), - version = GetRequestArgValue(request, "sc_version"), - pageSize = 50, - after = string.Empty - } - }; - - GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Item); - } - - // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library - string? json = response.Data?.Item?.Rendered.ToString(); - if (json == null) - { - errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); - } - else - { - content = _serializer.Deserialize(json); - if (_logger.IsEnabled(LogLevel.Debug)) - { - object? formattedDeserializeObject = JsonSerializer.Deserialize(json); - _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); - } - } - - if (response.Errors != null) - { - errors.AddRange( - response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); - } - - return content; - } - - private object GetRequestArgValue(SitecoreLayoutRequest request, string argName) - { - if (!request.ContainsKey("sc_request_headers_key") || - request["sc_request_headers_key"] is not Dictionary headers || - !headers.ContainsKey(argName)) - { - throw new ArgumentException($"Unable to parse arg:{argName} for Pages MetaData Render request."); - } - - return headers[argName].FirstOrDefault() ?? string.Empty; - } - - private async Task HandleLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors, SitecoreLayoutResponseContent? content) - { - GraphQLRequest layoutRequest = new() - { - Query = @" + Query = @" query LayoutQuery($path: String!, $language: String!, $site: String!) { layout(routePath: $path, language: $language, site: $site) { item { @@ -161,43 +53,48 @@ query LayoutQuery($path: String!, $language: String!, $site: String!) { } } }", - OperationName = "LayoutQuery", - Variables = new + OperationName = "LayoutQuery", + Variables = new + { + path = request.Path(), + language = requestLanguage, + site = request.SiteName() + } + }; + + GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + if (_logger.IsEnabled(LogLevel.Debug)) { - path = request.Path(), - language = requestLanguage, - site = request.SiteName() + _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Layout); } - }; - GraphQLResponse response = await _client.SendQueryAsync(layoutRequest).ConfigureAwait(false); - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Layout); - } + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library + string? json = response.Data?.Layout?.Item?.Rendered.ToString(); + if (json == null) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = _serializer.Deserialize(json); + if (_logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + } + } - // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract - Data can be null due to bad implementation of dependency library - string? json = response.Data?.Layout?.Item?.Rendered.ToString(); - if (json == null) - { - errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); - } - else - { - content = _serializer.Deserialize(json); - if (_logger.IsEnabled(LogLevel.Debug)) + if (response.Errors != null) { - object? formattedDeserializeObject = JsonSerializer.Deserialize(json); - _logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + errors.AddRange( + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); } } - if (response.Errors != null) + return new SitecoreLayoutResponse(request, errors) { - errors.AddRange( - response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); - } - - return content; + Content = content, + Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) + }; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs index 8cf82c0..0049390 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -8,25 +8,25 @@ public class PagesOptions /// /// Gets or sets the config endpoint for Pages MetaData mode. /// - public string ConfigEndpoint { get; set; } = "/api/editing/config"; + public string? ConfigEndpoint { get; set; } = "/api/editing/config"; /// /// Gets or sets the render endpoint for Pages MetaData mode. /// - public string RenderEndpoint { get; set; } = "/api/editing/render"; + public string? RenderEndpoint { get; set; } = "/api/editing/render"; /// /// Gets or sets the valid editing origin for all editing requests. /// - public string ValidEditingOrigin { get; set; } = "https://pages.sitecorecloud.io"; + public string? ValidEditingOrigin { get; set; } = "https://pages.sitecorecloud.io"; /// /// Gets or sets the valid origins for the head to run under. /// - public string ValidOrigins { get; set; } = string.Empty; + public string? ValidOrigins { get; set; } = string.Empty; /// /// Gets or sets the Editing Secret. /// - public string EditingSecret { get; set; } = string.Empty; + public string? EditingSecret { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs new file mode 100644 index 0000000..41b68a3 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs @@ -0,0 +1,30 @@ +namespace Sitecore.AspNetCore.SDK.Pages +{ + /// + /// Class used to stored constants referenced throughout the Pages project. + /// + public static class Constants + { + /// + /// Class used to hold the names of different Layout Clients used by the Pages project. + /// + public static class LayoutClients + { + /// + /// Name of the Pages Editing Layout Client. + /// + public const string Pages = "pages"; + } + + /// + /// Class used to hold the names of the different tag helpers defined in the Pages project. + /// + public static class SitecoreTagHelpers + { + /// + /// The HTML tag used to render the Editing Scripts tag helper. + /// + public const string EditScriptsHtmlTag = "sc-editingscripts"; + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index e18fd23..5fd07bb 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -1,9 +1,15 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; @@ -27,7 +33,7 @@ public static IApplicationBuilder UseSitecorePages(this IApplicationBuilder app) object? experienceEditorMarker = app.ApplicationServices.GetService(typeof(PagesMarkerService)); if (experienceEditorMarker != null) { - app.UseMiddleware(); + app.UseMiddleware(); app.UseMiddleware(); } @@ -38,9 +44,10 @@ public static IApplicationBuilder UseSitecorePages(this IApplicationBuilder app) /// Adds the Sitecore Experience Editor support services to the . /// /// The to add services to. + /// The ContextId for the environment being used. /// Configures the options. /// The so that additional calls can be chained. - public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRenderingEngineBuilder serviceBuilder, Action? options = null) + public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRenderingEngineBuilder serviceBuilder, string contextId, Action? options = null) { ArgumentNullException.ThrowIfNull(serviceBuilder); @@ -51,6 +58,7 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe } services.AddSingleton(); + services.AddSingleton(new GraphQLClientFactory(contextId)); if (options != null) { @@ -64,12 +72,38 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe MapRequest(httpRequest, layoutRequest, "mode"); MapRequest(httpRequest, layoutRequest, "sc_itemid"); MapRequest(httpRequest, layoutRequest, "sc_version"); + MapRequest(httpRequest, layoutRequest, "sc_lang"); + MapRequest(httpRequest, layoutRequest, "sc_site"); + MapRequest(httpRequest, layoutRequest, "sc_layoutKind"); + MapRequest(httpRequest, layoutRequest, "secret"); + MapRequest(httpRequest, layoutRequest, "tenant_id"); + MapRequest(httpRequest, layoutRequest, "route"); }); })); return serviceBuilder; } + /// + /// Registers an HTTP request handler for the Sitecore layout service client. + /// + /// The to configure. + /// The so that additional calls can be chained. + + public static ISitecoreLayoutClientBuilder AddSitecorePagesHandler( + this ISitecoreLayoutClientBuilder builder) + { + string name = Constants.LayoutClients.Pages; + builder.AddHandler(name, sp + => ActivatorUtilities.CreateInstance( + sp, + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>())); + + return builder; + } + private static void MapRequest(HttpRequest httpRequest, SitecoreLayoutRequest layoutRequest, string paramName) { if (httpRequest.Query == null || !httpRequest.Query.ContainsKey(paramName)) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs new file mode 100644 index 0000000..1dc35b9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs @@ -0,0 +1,34 @@ +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; +using Sitecore.AspNetCore.SDK.GraphQL.Extensions; + +namespace Sitecore.AspNetCore.SDK.Pages.GraphQL; + +/// +/// GraphQLClientFactory used to generate instances of of GraphQLClients authenticated using a ContextId. +/// The contextId for the envionment being used. +/// +public class GraphQLClientFactory(string contextId) + : IGraphQLClientFactory +{ + private readonly string contextId = contextId; + + /// + public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, string editMode) + { + uri ??= new Uri("https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1"); + uri = uri.AddQueryString("sitecoreContextId", contextId)!; + + GraphQLHttpClient client = new(uri, new SystemTextJsonSerializer()); + client.HttpClient.DefaultRequestHeaders.Add("sc_layoutKind", layoutKind); + client.HttpClient.DefaultRequestHeaders.Add("sc_editmode", editMode); + return client; + } + + /// + public IGraphQLClient GenerateClient(string layoutKind, string editMode) + { + return GenerateClient(null, layoutKind, editMode); + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs new file mode 100644 index 0000000..2cefbaf --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs @@ -0,0 +1,26 @@ +using GraphQL.Client.Abstractions; + +namespace Sitecore.AspNetCore.SDK.Pages.GraphQL; + +/// +/// Interface used to define the contract that IGraphQlClientFactories need to adhere to. +/// +public interface IGraphQLClientFactory +{ + /// + /// Method used to generate an instance of . + /// + /// GraphQl endpoint uri. + /// The layout type for this request, shared or final. + /// The edit mode version for this client. + /// Concrete implementation of interface. + public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, string editMode); + + /// + /// Method used to generate an instance of . + /// + /// The layout type for this request, shared or final. + /// The edit mode version for this client. + /// Concrete implementation of interface. + public IGraphQLClient GenerateClient(string layoutKind, string editMode); +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs similarity index 66% rename from src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs rename to src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs index d2779cb..337276d 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesConfigMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Models; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; namespace Sitecore.AspNetCore.SDK.Pages.Middleware; @@ -12,17 +13,17 @@ namespace Sitecore.AspNetCore.SDK.Pages.Middleware; /// and wraps the response HTML in a JSON format. /// /// -/// Initializes a new instance of the class. +/// Initializes a new instance of the class. /// /// The next middleware to call. /// The Sitecore Pages configuration options. /// The to use for logging. /// The RenderingEngineOptions, used to retriece a list of all registered renderings for the applications -public class PagesConfigMiddleware(RequestDelegate next, IOptions options, ILogger logger, IOptions renderingEngineOptions) +public class PageSetupMiddleware(RequestDelegate next, IOptions options, ILogger logger, IOptions renderingEngineOptions) { private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly RenderingEngineOptions renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); /// @@ -39,9 +40,57 @@ public async Task Invoke(HttpContext httpContext) return; } + if (IsValidPagesRenderRequest(httpContext.Request)) + { + logger.LogDebug("Processing valid Pages Render request"); + + PerformPagesRedirect(httpContext, ParseQueryStringArgs(httpContext.Request)); + + return; + } + await next(httpContext).ConfigureAwait(false); } + private void PerformPagesRedirect(HttpContext httpContext, PagesRenderArgs args) + { + httpContext.Response.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; + httpContext.Response.Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}&sc_lang={args.Language}&sc_site={args.Site}&sc_layoutKind={args.LayoutKind}&secret={args.EditingSecret}&tenant_id={args.TenantId}&route={args.Route}", permanent: false); + } + + private bool IsValidPagesRenderRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.RenderEndpoint, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (!IsValidEditingSecret(httpRequest)) + { + logger.LogError("Invalid Pages Editing Secret Value"); + return false; + } + + return true; + } + + private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) + { + return new PagesRenderArgs + { + ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, + EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, + Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, + LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, + Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, + Route = request.Query["route"].FirstOrDefault() ?? string.Empty, + Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, + Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0, + TenantId = request.Query["tenant_id"].FirstOrDefault() ?? string.Empty, + }; + } + private async Task BuildResponse(HttpResponse httpResponse) { httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 4ac3076..7df516e 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -1,9 +1,15 @@ using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; using Sitecore.AspNetCore.SDK.Pages.Configuration; -using Sitecore.AspNetCore.SDK.Pages.Models; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.Pages.Middleware; @@ -12,85 +18,83 @@ namespace Sitecore.AspNetCore.SDK.Pages.Middleware; /// and wraps the response HTML in a JSON format. /// /// -/// Initializes a new instance of the class. +/// Initializes a new instance of the class. /// /// The next middleware to call. /// The Sitecore Pages configuration options. +/// The to map the HttpRequest to a Layout Service request. +/// The layout service client. /// The to use for logging. -public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ILogger logger) +public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService, ILogger logger) { private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly ISitecoreLayoutRequestMapper _requestMapper = requestMapper ?? throw new ArgumentNullException(nameof(requestMapper)); + private readonly ISitecoreLayoutClient layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// /// The middleware Invoke method. /// /// The current . + /// The current . + /// The current . /// A Task to support async calls. - public async Task Invoke(HttpContext httpContext) + public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) { - if (IsValidPagesRenderRequest(httpContext.Request)) + ArgumentNullException.ThrowIfNull(httpContext); + ArgumentNullException.ThrowIfNull(viewComponentHelper); + ArgumentNullException.ThrowIfNull(htmlHelper); + + if (IsEditingRequest(httpContext)) { - logger.LogDebug("Processing valid Pages Render request"); + // this protects from multiple time executions when Global and Attribute based configurations are used at the same time. + if (httpContext.Items.ContainsKey(nameof(PagesRenderMiddleware))) + { + throw new ApplicationException("PagesRenderMiddleware already registered. Have you "); + } - PerformPagesRedirect(httpContext, ParseQueryStringArgs(httpContext.Request)); + if (httpContext.GetSitecoreRenderingContext() == null) + { + SitecoreLayoutResponse response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(false); - return; - } + SitecoreRenderingContext scContext = new() + { + Response = response, + RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) + }; - await next(httpContext).ConfigureAwait(false); - } + httpContext.SetSitecoreRenderingContext(scContext); + } + else + { + ISitecoreRenderingContext? scContext = httpContext.GetSitecoreRenderingContext(); + if (scContext != null) + { + scContext.RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); + } + } - private void PerformPagesRedirect(HttpContext httpContext, PagesRenderArgs args) - { - httpContext.Response.Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}", permanent: false); - } + httpContext.Items.Add(nameof(PagesRenderMiddleware), null); + } - private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) - { - return new PagesRenderArgs - { - ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, - EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, - Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, - LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, - Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, - Route = request.Query["route"].FirstOrDefault() ?? string.Empty, - Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, - Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0 - }; + await next(httpContext).ConfigureAwait(false); } - - private bool IsValidEditingSecret(HttpRequest httpRequest) + private static bool IsEditingRequest(HttpContext context) { - if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + if (context.Request.Query.TryGetValue("mode", out var mode)) { - string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; - if (editingSecret == options.EditingSecret) - { - return true; - } + return mode == "edit"; } return false; } - private bool IsValidPagesRenderRequest(HttpRequest httpRequest) + private async Task GetSitecoreLayoutResponse(HttpContext httpContext) { - ArgumentNullException.ThrowIfNull(httpRequest); - if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.RenderEndpoint, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!IsValidEditingSecret(httpRequest)) - { - logger.LogError("Invalid Pages Editing Secret Value"); - return false; - } - - return true; + SitecoreLayoutRequest request = requestMapper.Map(httpContext.Request); + ArgumentNullException.ThrowIfNull(request); + return await layoutService.Request(request, Constants.LayoutClients.Pages).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs index 03a9bcf..683f009 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs @@ -46,5 +46,10 @@ public class PagesRenderArgs /// Gets or sets the route to the item within the site. /// public string Route { get; set; } = string.Empty; + + /// + /// Gets or sets the ID of the tenant that editing is being used on. + /// + public string TenantId { get; set; } = string.Empty; } } diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs similarity index 62% rename from src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs rename to src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs index b50da86..8a7370b 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -1,4 +1,6 @@ -namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// /// Layout Service GraphQL Response. diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs new file mode 100644 index 0000000..98a435f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -0,0 +1,156 @@ +using System.Text.Json; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.Pages.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// +/// Initializes a new instance of the class. +/// +/// The to use for logging. +/// The GraphQlClientFactory used to generate instances of the GraphQl client. +/// The serializer to handle response data. +public class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, + ISitecoreLayoutSerializer serializer, + ILogger logger) + : ILayoutRequestHandler +{ + private readonly IGraphQLClientFactory clientFactory = clientFactory; + private readonly ISitecoreLayoutSerializer serializer = serializer; + private readonly ILogger logger = logger; + + /// + public async Task Request(SitecoreLayoutRequest request, string handlerName) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentException.ThrowIfNullOrWhiteSpace(handlerName); + + if (!IsEditingRequest(request)) + { + throw new ArgumentException("GraphQLEditingServiceHandler: Error attempting to process non-editing request"); + } + + List errors = []; + SitecoreLayoutResponseContent? content = null; + + string? requestLanguage = request.Language(); + + if (string.IsNullOrWhiteSpace(requestLanguage)) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = await HandleEditingLayoutRequest(request, requestLanguage, errors); + } + + return new SitecoreLayoutResponse(request, errors) + { + Content = content, + Metadata = new Dictionary().ToLookup(k => k.Key, v => v.Value) + }; + } + + private static bool IsEditingRequest(SitecoreLayoutRequest request) + { + if (!request.ContainsKey("sc_request_headers_key") || + request["sc_request_headers_key"] is not Dictionary headers || + !headers.ContainsKey("mode")) + { + return false; + } + + return headers["mode"].Contains("edit"); + } + + private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) + { + GraphQLRequest layoutRequest = new() + { + Query = @" + query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $version: String, $after: String, $pageSize: Int = 10) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + ", + OperationName = "EditingQuery", + Variables = new + { + itemId = GetRequestArgValue(request, "sc_itemid"), + language = requestLanguage, + siteName = request.SiteName(), + version = GetRequestArgValue(request, "sc_version"), + pageSize = 50, + after = string.Empty + } + }; + + IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode")); + GraphQLResponse response = await client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + + if (logger.IsEnabled(LogLevel.Debug)) + { + logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Item); + } + + SitecoreLayoutResponseContent? content = null; + string? json = response.Data?.Item?.Rendered.ToString(); + if (json == null) + { + errors.Add(new ItemNotFoundSitecoreLayoutServiceClientException()); + } + else + { + content = serializer.Deserialize(json); + if (logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + } + } + + if (response.Errors != null) + { + errors.AddRange( + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + } + + return content; + } + + private string GetRequestArgValue(SitecoreLayoutRequest request, string argName) + { + if (!request.ContainsKey("sc_request_headers_key") || + request["sc_request_headers_key"] is not Dictionary headers || + !headers.ContainsKey(argName)) + { + throw new ArgumentException($"Unable to parse arg:{argName} for Pages MetaData Render request."); + } + + return headers[argName].FirstOrDefault() ?? string.Empty; + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs new file mode 100644 index 0000000..7615a4a --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs @@ -0,0 +1,51 @@ +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.Pages.Response; + +/// +/// Class used to store the ClientData editing context, used to enable Pages functionality. +/// +public class CanvasState +{ + /// + /// Gets or sets the Id of the item being edited. + /// + [JsonPropertyName("itemId")] + public string? ItemId { get; set; } + + /// + /// Gets or sets the Version of the item being edited. + /// + [JsonPropertyName("itemVersion")] + public int? ItemVersion { get; set; } + + /// + /// Gets or sets the name of the site being edited. + /// + [JsonPropertyName("siteName")] + public string? SiteName { get; set; } + + /// + /// Gets or sets the language of the item being edited. + /// + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// + /// Gets or sets the Id of the Device being edited. + /// + [JsonPropertyName("deviceId")] + public string? DeviceId { get; set; } + + /// + /// Gets or sets the current page mode. + /// + [JsonPropertyName("pageMode")] + public string? PageMode { get; set; } + + /// + /// Gets or sets the current id of the Varient being edited. + /// + [JsonPropertyName("varient")] + public string? Varient { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs new file mode 100644 index 0000000..2fe1b0e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.Pages.Response; + +/// +/// Class used to store the ClientData editing context, used to enable Pages functionality. +/// +public class ClientData +{ + /// + /// Gets or sets the Canvas State data of the Editing Request + /// + [JsonPropertyName("hrz-canvas-state")] + public CanvasState? CanvasState { get; set; } + + /// + /// Gets or sets the verification token used by the Pages canvas. + /// + [JsonPropertyName("hrz-canvas-verification-token")] + public string? CanvasVerificationToken { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/EditingContext.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/EditingContext.cs new file mode 100644 index 0000000..0fa9aa0 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/EditingContext.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.Pages.Response; + +/// +/// Class used to extend the standard Sitecore Context with editing specific context data. +/// +public class EditingContext : Context +{ + /// + /// Gets or sets the Client data property used to hold the editing specific client data. + /// + [JsonPropertyName("clientData")] + public ClientData? ClientData { get; set; } + + /// + /// Gets or sets the ClientScripts property used to store the scripts that need to be included in the client app for Pages to function. + /// + [JsonPropertyName("clientScripts")] + public string[]? ClientScripts { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj index 8484286..683e8a8 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj +++ b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs new file mode 100644 index 0000000..d2869e9 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -0,0 +1,74 @@ +using System.Resources; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.Pages.Response; +using Sitecore.AspNetCore.SDK.RenderingEngine; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.Pages.TagHelpers +{ + /// + /// EditingScriptsTagHelper, used to output the script tags into the page header when running in Chromes editing mode. + /// + [HtmlTargetElement(Constants.SitecoreTagHelpers.EditScriptsHtmlTag)] + public class EditingScriptsTagHelper : TagHelper + { + /// + /// Gets or sets the current view context for the tag helper. + /// + [HtmlAttributeNotBound] + [ViewContext] + public ViewContext? ViewContext { get; set; } + + /// + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext() ?? + throw new NullReferenceException("EditingScriptsTagHelper: Sitecore RenderingContext is Null"); + + output.TagName = string.Empty; + string html = string.Empty; + + if (renderingContext?.Response?.Content?.Sitecore?.Context?.IsEditing ?? false) + { + JsonDocument doc = JsonDocument.Parse(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); + if (doc == null) + { + throw new NullReferenceException("EditingScriptsTagHelper: Unable to process ContextRawData"); + } + + EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); + if (editingContext == null) + { + return; + } + + foreach (string script in editingContext.ClientScripts ?? []) + { + html += $""; + } + + html += $@" + + "; + + html += $""; + } + + output.Content.SetHtmlContent(html); + } + } +} From 1c8752eebd9d6819b2781a448d7f5940f7a4b910 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 16 Dec 2024 14:11:38 +0000 Subject: [PATCH 04/38] Wired up Placeholder and Rendering chromes --- .../GraphQL/GraphQLClientFactory.cs | 6 +- .../GraphQL/IGraphQLClientFactory.cs | 4 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 89 ++++++++++++++++++- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs index 1dc35b9..9d23d29 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs @@ -15,19 +15,19 @@ public class GraphQLClientFactory(string contextId) private readonly string contextId = contextId; /// - public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, string editMode) + public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, bool editMode) { uri ??= new Uri("https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1"); uri = uri.AddQueryString("sitecoreContextId", contextId)!; GraphQLHttpClient client = new(uri, new SystemTextJsonSerializer()); client.HttpClient.DefaultRequestHeaders.Add("sc_layoutKind", layoutKind); - client.HttpClient.DefaultRequestHeaders.Add("sc_editmode", editMode); + client.HttpClient.DefaultRequestHeaders.Add("sc_editmode", editMode.ToString()); return client; } /// - public IGraphQLClient GenerateClient(string layoutKind, string editMode) + public IGraphQLClient GenerateClient(string layoutKind, bool editMode) { return GenerateClient(null, layoutKind, editMode); } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs index 2cefbaf..7e91061 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs @@ -14,7 +14,7 @@ public interface IGraphQLClientFactory /// The layout type for this request, shared or final. /// The edit mode version for this client. /// Concrete implementation of interface. - public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, string editMode); + public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, bool editMode); /// /// Method used to generate an instance of . @@ -22,5 +22,5 @@ public interface IGraphQLClientFactory /// The layout type for this request, shared or final. /// The edit mode version for this client. /// Concrete implementation of interface. - public IGraphQLClient GenerateClient(string layoutKind, string editMode); + public IGraphQLClient GenerateClient(string layoutKind, bool editMode); } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 98a435f..bba69a9 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -1,4 +1,8 @@ -using System.Text.Json; +using System.Collections.Generic; +using System.Net.NetworkInformation; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Xml.Linq; using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; @@ -7,8 +11,10 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.Pages.GraphQL; +using static System.Net.Mime.MediaTypeNames; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -109,7 +115,7 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve } }; - IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode")); + IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode") == "edit"); GraphQLResponse response = await client.SendQueryAsync(layoutRequest).ConfigureAwait(false); if (logger.IsEnabled(LogLevel.Debug)) @@ -126,6 +132,9 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve else { content = serializer.Deserialize(json); + + GenerateMetaDataChromes(content); + if (logger.IsEnabled(LogLevel.Debug)) { object? formattedDeserializeObject = JsonSerializer.Deserialize(json); @@ -142,7 +151,81 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve return content; } - private string GetRequestArgValue(SitecoreLayoutRequest request, string argName) + private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? content) + { + foreach (var placeholder in content.Sitecore.Route.Placeholders) + { + string name = placeholder.Key; + Placeholder placeholderFeatures = placeholder.Value; + + content.Sitecore.Route.Placeholders[name] = ProcessPlaceholder(name, Guid.Empty.ToString(), placeholderFeatures); + } + } + + private static Placeholder ProcessPlaceholder(string name, string id, Placeholder placeholderFeatures) + { + Placeholder updatedPlaceholders = new Placeholder(); + + AddOpeningChrome("placeholder", $"{name}_{id}", updatedPlaceholders); + + foreach (var feature in placeholderFeatures) + { + if (feature is Component component) + { + AddOpeningChrome("rendering", component.Id, updatedPlaceholders); + + updatedPlaceholders.Add(feature); + + foreach (var componentPlaceholder in component.Placeholders) + { + { + string componentPlaceholderName = componentPlaceholder.Key; + Placeholder componentPlaceholderFeatures = componentPlaceholder.Value; + + component.Placeholders[componentPlaceholderName] = ProcessPlaceholder(name, component.Id, componentPlaceholderFeatures); + } + } + + AddClosingChrome("rendering", updatedPlaceholders); + } + } + + AddClosingChrome("placeholder", updatedPlaceholders); + + return updatedPlaceholders; + } + + + private static void AddClosingChrome(string type, Placeholder placeholderFeatures) + { + EditableChrome placeHolderClosingChrome = new EditableChrome + { + Attributes = + { + { "chrometype", type }, + { "class", "scpm" }, + { "kind", "close" }, + } + }; + placeholderFeatures.Add(placeHolderClosingChrome); + } + + private static void AddOpeningChrome(string type, string id, Placeholder placeholderFeatures) + { + EditableChrome placeHolderOpeningChrome = new EditableChrome + { + Attributes = + { + { "chrometype", type }, + { "class", "scpm" }, + { "kind", "open" }, + { "id", id }, + } + }; + placeholderFeatures.Add(placeHolderOpeningChrome); + } + + private static string GetRequestArgValue(SitecoreLayoutRequest request, string argName) { if (!request.ContainsKey("sc_request_headers_key") || request["sc_request_headers_key"] is not Dictionary headers || From bd0dcde7855c4de7023349ed2c751a5003a7a89e Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 22 Jan 2025 15:31:44 +1100 Subject: [PATCH 05/38] Metadata editing now working --- .../Response/Model/DataSource.cs | 30 +++ .../Response/Model/EditableField.cs | 18 ++ .../Response/Model/IEditableField.cs | 11 + .../Response/Model/MetaData.cs | 33 +++ .../Response/Model/WrappedEditableField.cs | 2 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 250 +++++++++++------- .../TagHelpers/Fields/DateTagHelper.cs | 17 +- .../TagHelpers/Fields/ImageTagHelper.cs | 16 +- .../TagHelpers/Fields/LinkTagHelper.cs | 39 ++- .../TagHelpers/Fields/NumberTagHelper.cs | 17 +- .../TagHelpers/Fields/RichTextTagHelper.cs | 26 +- .../TagHelpers/Fields/TextFieldTagHelper.cs | 22 +- 12 files changed, 364 insertions(+), 117 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/DataSource.cs create mode 100644 src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/DataSource.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/DataSource.cs new file mode 100644 index 0000000..39b048e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/DataSource.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Class used to define an items datasource information. +/// +public class DataSource +{ + /// + /// Gets or sets the Id. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the Language. + /// + public string Language { get; set; } = string.Empty; + + /// + /// Gets or sets the Revision. + /// + public string Revision { get; set; } = string.Empty; + + /// + /// Gets or sets the Version. + /// + public int Version { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs index 62fa1d3..f6a74ad 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs @@ -14,4 +14,22 @@ public class EditableField [DataMember(Name = "editable")] [JsonPropertyName("editable")] public string EditableMarkup { get; set; } = string.Empty; + + /// + /// Gets or Sets the id of the Field. + /// + [DataMember(Name = "Id")] + [JsonPropertyName("Id")] + public string Id { get; set; } = string.Empty; + + /// + public EditableChrome? OpeningChrome { get; set; } + + /// + public EditableChrome? ClosingChrome { get; set; } + + /// + /// Gets or Sets the MetaSata for the Field. + /// + public MetaData? MetaData { get; set; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs index fcdda5c..590f044 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs @@ -9,4 +9,15 @@ public interface IEditableField : IField /// Gets or sets the HTML markup for this when editing. /// public string EditableMarkup { get; set; } + + /// + /// Gets or sets the EditableChrome used to render the opening chrome for this field. + /// + public EditableChrome? OpeningChrome { get; set; } + + /// + /// Gets or sets the EditableChrome used to render the closing chrome for this field. + /// + + public EditableChrome? ClosingChrome { get; set; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs new file mode 100644 index 0000000..ac21885 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs @@ -0,0 +1,33 @@ +namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +/// +/// Class used to define an items metadata information. +/// +public class MetaData +{ + /// + /// Gets or sets the title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the FieldId. + /// + public string FieldId { get; set; } = string.Empty; + + /// + /// Gets or sets the FieldType. + /// + public string FieldType { get; set; } = string.Empty; + + /// + /// Gets or sets the RawValue. + /// + public string RawValue { get; set; } = string.Empty; + + /// + /// Gets or sets the DataSource. + /// + public DataSource? DataSource { get; set; } + +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs index 1a9f00b..1b63361 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/WrappedEditableField.cs @@ -8,7 +8,7 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; /// that contains a value that can be edited using wrapped HTML markup. /// /// The value type. -public class WrappedEditableField : Field, IWrappedEditableField +public class WrappedEditableField : EditableField, IWrappedEditableField { /// [DataMember(Name = "editableFirstPart")] diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index bba69a9..76436d2 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Net.NetworkInformation; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Xml.Linq; +using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; @@ -13,8 +9,8 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.GraphQL; -using static System.Net.Mime.MediaTypeNames; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -78,6 +74,160 @@ private static bool IsEditingRequest(SitecoreLayoutRequest request) return headers["mode"].Contains("edit"); } + private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? content) + { + if (content?.Sitecore?.Route == null) + { + return; + } + + foreach (var placeholder in content.Sitecore.Route.Placeholders) + { + string name = placeholder.Key; + Placeholder placeholderFeatures = placeholder.Value; + + content.Sitecore.Route.Placeholders[name] = ProcessPlaceholder(name, Guid.Empty.ToString(), placeholderFeatures); + } + } + + private static Placeholder ProcessPlaceholder(string name, string id, Placeholder placeholderFeatures) + { + Placeholder updatedPlaceholders = []; + + AddPlaceholderOpeningChrome(name, id, updatedPlaceholders); + + foreach (var feature in placeholderFeatures) + { + if (feature is Component component) + { + AddRenderingOpeningChrome(updatedPlaceholders, component); + + var updatedFields = new Dictionary(); + foreach (var field in component.Fields) + { + ProcessField(updatedFields, field); + } + + component.Fields = updatedFields; + + updatedPlaceholders.Add(component); + + foreach (var componentPlaceholder in component.Placeholders) + { + { + string componentPlaceholderName = componentPlaceholder.Key; + Placeholder componentPlaceholderFeatures = componentPlaceholder.Value; + + component.Placeholders[componentPlaceholderName] = ProcessPlaceholder("container-{*}", component.Id, componentPlaceholderFeatures); + } + } + + AddRenderingClosingChrome(updatedPlaceholders); + } + } + + AddPlaceholderClosingChrome(updatedPlaceholders); + + return updatedPlaceholders; + } + + private static void AddRenderingClosingChrome(Placeholder updatedPlaceholders) + { + updatedPlaceholders.Add(GenerateEditableChrome("rendering", "close", string.Empty, string.Empty)); + } + + private static void AddRenderingOpeningChrome(Placeholder updatedPlaceholders, Component component) + { + updatedPlaceholders.Add(GenerateEditableChrome("rendering", "open", component.Id, string.Empty)); + } + + private static void AddPlaceholderClosingChrome(Placeholder updatedPlaceholders) + { + updatedPlaceholders.Add(GenerateEditableChrome("placeholder", "close", string.Empty, string.Empty)); + } + + private static void AddPlaceholderOpeningChrome(string name, string id, Placeholder updatedPlaceholders) + { + updatedPlaceholders.Add(GenerateEditableChrome("placeholder", "open", $"{name}_{id}", string.Empty)); + } + + private static void ProcessField(Dictionary updatedFields, KeyValuePair field) + { + if (field.Value is JsonSerializedField serialisedField && field.Key != "CustomContent") + { + var editableField = serialisedField.Read>(); + if (editableField == null) + { + return; + } + + object openingChromeContent = new + { + datasource = new + { + id = editableField?.MetaData.DataSource.Id, + language = editableField?.MetaData.DataSource.Language, + revision = editableField?.MetaData.DataSource.Revision, + version = editableField?.MetaData.DataSource.Version + }, + title = editableField?.MetaData.Title, + fieldId = editableField?.MetaData.FieldId, + fieldType = editableField?.MetaData.FieldType, + rawValue = editableField?.MetaData.RawValue + }; + + editableField.OpeningChrome = GenerateEditableChrome("field", "open", string.Empty, JsonSerializer.Serialize(openingChromeContent)); + editableField.ClosingChrome = GenerateEditableChrome("field", "close", string.Empty, string.Empty); + + var editableFieldWithChromesJson = JsonSerializer.SerializeToDocument(editableField); + var updatedJsonSerialisedField = new JsonSerializedField(editableFieldWithChromesJson); + + updatedFields.Add(field.Key, updatedJsonSerialisedField); + } + else + { + updatedFields.Add(field.Key, field.Value); + } + } + + private static EditableChrome GenerateEditableChrome(string chrometype, string kind, string id, string content) + { + EditableChrome editableChrome = new EditableChrome + { + Attributes = + { + { "chrometype", chrometype }, + { "class", "scpm" }, + { "kind", kind }, + { "type", "text/sitecore" } + } + }; + + if (id != string.Empty) + { + editableChrome.Attributes.Add("id", id); + } + + if (content != string.Empty) + { + editableChrome.Content = content; + } + + return editableChrome; + } + + private static string GetRequestArgValue(SitecoreLayoutRequest request, string argName) + { + if (!request.ContainsKey("sc_request_headers_key") || + request["sc_request_headers_key"] is not Dictionary headers || + !headers.ContainsKey(argName)) + { + throw new ArgumentException($"Unable to parse arg:{argName} for Pages MetaData Render request."); + } + + return headers[argName].FirstOrDefault() ?? string.Empty; + } + private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) { GraphQLRequest layoutRequest = new() @@ -150,90 +300,4 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve return content; } - - private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? content) - { - foreach (var placeholder in content.Sitecore.Route.Placeholders) - { - string name = placeholder.Key; - Placeholder placeholderFeatures = placeholder.Value; - - content.Sitecore.Route.Placeholders[name] = ProcessPlaceholder(name, Guid.Empty.ToString(), placeholderFeatures); - } - } - - private static Placeholder ProcessPlaceholder(string name, string id, Placeholder placeholderFeatures) - { - Placeholder updatedPlaceholders = new Placeholder(); - - AddOpeningChrome("placeholder", $"{name}_{id}", updatedPlaceholders); - - foreach (var feature in placeholderFeatures) - { - if (feature is Component component) - { - AddOpeningChrome("rendering", component.Id, updatedPlaceholders); - - updatedPlaceholders.Add(feature); - - foreach (var componentPlaceholder in component.Placeholders) - { - { - string componentPlaceholderName = componentPlaceholder.Key; - Placeholder componentPlaceholderFeatures = componentPlaceholder.Value; - - component.Placeholders[componentPlaceholderName] = ProcessPlaceholder(name, component.Id, componentPlaceholderFeatures); - } - } - - AddClosingChrome("rendering", updatedPlaceholders); - } - } - - AddClosingChrome("placeholder", updatedPlaceholders); - - return updatedPlaceholders; - } - - - private static void AddClosingChrome(string type, Placeholder placeholderFeatures) - { - EditableChrome placeHolderClosingChrome = new EditableChrome - { - Attributes = - { - { "chrometype", type }, - { "class", "scpm" }, - { "kind", "close" }, - } - }; - placeholderFeatures.Add(placeHolderClosingChrome); - } - - private static void AddOpeningChrome(string type, string id, Placeholder placeholderFeatures) - { - EditableChrome placeHolderOpeningChrome = new EditableChrome - { - Attributes = - { - { "chrometype", type }, - { "class", "scpm" }, - { "kind", "open" }, - { "id", id }, - } - }; - placeholderFeatures.Add(placeHolderOpeningChrome); - } - - private static string GetRequestArgValue(SitecoreLayoutRequest request, string argName) - { - if (!request.ContainsKey("sc_request_headers_key") || - request["sc_request_headers_key"] is not Dictionary headers || - !headers.ContainsKey(argName)) - { - throw new ArgumentException($"Unable to parse arg:{argName} for Pages MetaData Render request."); - } - - return headers[argName].FirstOrDefault() ?? string.Empty; - } -} +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs index c52a0c9..0f24e89 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -13,8 +14,10 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute)] -public class DateTagHelper : TagHelper +public class DateTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + /// /// Gets or sets the model value. /// @@ -68,6 +71,16 @@ public override void Process(TagHelperContext context, TagHelperOutput output) HtmlString html = outputEditableMarkup ? new HtmlString(field.EditableMarkup) : new HtmlString(formattedDate); - output.Content.SetHtmlContent(html); + if (field.OpeningChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + } + + output.Content.AppendHtml(html); + + if (field.ClosingChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + } } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs index 3044a67..ced2efc 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs @@ -6,6 +6,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -16,7 +17,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.ImageTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] [HtmlTargetElement("img", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] [HtmlTargetElement("img", Attributes = RenderingEngineConstants.SitecoreTagHelpers.ImageTagHelperAttribute)] -public class ImageTagHelper : TagHelper +public class ImageTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { private const string ImgTag = "img"; private const string ScrAttribute = "src"; @@ -28,6 +29,7 @@ public class ImageTagHelper : TagHelper private const string VSpaceAttribute = "vspace"; private const string TitleAttribute = "title"; private const string BorderAttribute = "border"; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; /// /// Gets or sets the model value. @@ -80,7 +82,17 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { if (output.TagName == null) { - output.Content.SetHtmlContent(GenerateImage(field, output)); + if (field.OpeningChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + } + + output.Content.AppendHtml(GenerateImage(field, output)); + + if (field.ClosingChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + } } else { diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs index 350f552..58b7747 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -15,7 +16,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.LinkHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.LinkTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] [HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] [HtmlTargetElement("a", Attributes = RenderingEngineConstants.SitecoreTagHelpers.LinkTagHelperAttribute)] -public class LinkTagHelper : TagHelper +public class LinkTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { private const string HrefAttribute = "href"; private const string TargetAttribute = "target"; @@ -25,6 +26,7 @@ public class LinkTagHelper : TagHelper private const string RelAttribute = "rel"; private const string BlankValue = "_blank"; private const string AnchorValue = "#"; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; /// /// Gets or sets the model value. @@ -50,7 +52,9 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ArgumentNullException.ThrowIfNull(output); HyperLinkField? field = LinkModel ?? For?.Model as HyperLinkField; - bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field.EditableMarkupLast); + bool outputEditableMarkup = Editable && + ((!string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field.EditableMarkupLast)) || (field.OpeningChrome != null && field.ClosingChrome != null)); + if (field == null || (string.IsNullOrWhiteSpace(field.Value.Href) && !outputEditableMarkup)) { return; @@ -71,11 +75,10 @@ public override void Process(TagHelperContext context, TagHelperOutput output) } } - private static void RenderMarkup(TagHelperOutput output, HyperLinkField field) + private void RenderMarkup(TagHelperOutput output, HyperLinkField field) { if (output.TagName == null) { - // generate full anchor markup output.Content.SetHtmlContent(GenerateLink(field.Value, output)); } else @@ -113,12 +116,30 @@ private static void RenderMarkup(TagHelperOutput output, HyperLinkField field) } } - private static void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) + private void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) { - DefaultTagHelperContent content = new(); - _ = content.AppendHtml(new HtmlString(field.EditableMarkupFirst)); - _ = content.AppendHtml(new HtmlString(field.EditableMarkupLast)); - output.Content.SetHtmlContent(content); + if (field.OpeningChrome != null && field.ClosingChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + + if (field.Value.Href == string.Empty) + { + output.Content.AppendHtml("[No text in field]"); + } + else + { + output.Content.AppendHtml(GenerateLink(field.Value, output)); + } + + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + } + else + { + DefaultTagHelperContent content = new(); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupFirst)); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupLast)); + output.Content.SetHtmlContent(content); + } } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs index 6ac8f57..5d2ec75 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -12,8 +13,10 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute)] -public class NumberTagHelper : TagHelper +public class NumberTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + /// /// Gets or sets the model value. /// @@ -70,6 +73,16 @@ public override void Process(TagHelperContext context, TagHelperOutput output) bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); string value = outputEditableMarkup ? field.EditableMarkup : formattedNumber; - output.Content.SetHtmlContent(value); + if (field.OpeningChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + } + + output.Content.AppendHtml(value); + + if (field.ClosingChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + } } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs index 6a2b9e8..3de128a 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -13,8 +14,10 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement(RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag, Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute, TagStructure = TagStructure.NormalOrSelfClosing)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute)] -public class RichTextTagHelper : TagHelper +public class RichTextTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + /// /// Gets or sets the model value. /// @@ -48,11 +51,24 @@ public override void Process(TagHelperContext context, TagHelperOutput output) return; } + string html = string.Empty; + if (Editable && richTextField.OpeningChrome != null) + { + html += chromeRenderer.Render(richTextField.OpeningChrome); + html += "
"; + } + bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(richTextField.EditableMarkup); - HtmlString html = outputEditableMarkup - ? new HtmlString(richTextField.EditableMarkup) - : new HtmlString(richTextField.Value); + html += outputEditableMarkup + ? richTextField.EditableMarkup + : richTextField.Value; + + if (Editable && richTextField.ClosingChrome != null) + { + html += "
"; + html += chromeRenderer.Render(richTextField.ClosingChrome); + } - output.Content.SetHtmlContent(html); + output.Content.SetHtmlContent(new HtmlString(html)); } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs index 9291be2..3756a39 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs @@ -1,7 +1,9 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; @@ -9,8 +11,10 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; /// Tag helper that renders text for a Sitecore . ///
[HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] -public partial class TextFieldTagHelper : TagHelper +public partial class TextFieldTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + /// /// Gets or sets the model value. /// @@ -42,14 +46,26 @@ public override void Process(TagHelperContext context, TagHelperOutput output) bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); string value = outputEditableMarkup ? field.EditableMarkup : field.Value; + if (Editable && field.OpeningChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + output.Content.AppendHtml("
"); + } + if (outputEditableMarkup || (ConvertNewLines && NewLineRegex().IsMatch(value))) { value = NewLineRegex().Replace(value, "
"); - output.Content.SetHtmlContent(value); + output.Content.AppendHtml(value); } else { - output.Content.SetContent(value); + output.Content.Append(value); + } + + if (Editable && field.ClosingChrome != null) + { + output.Content.AppendHtml("
"); + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); } } From 2b99fc674b80045f4f20366b26524a88c24cb018 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 29 Jan 2025 13:58:23 +1100 Subject: [PATCH 06/38] Wired up EditingDictionaryQuery requests --- .../GraphQL/EditingDictionaryResponse.cs | 12 +++ .../GraphQL/EditingLayoutQueryResponse.cs | 8 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 99 +++++++++++++++---- .../Request/Handlers/GraphQL/PageInfo.cs | 17 ++++ .../Request/Handlers/GraphQL/Site.cs | 12 +++ .../Request/Handlers/GraphQL/SiteInfo.cs | 12 +++ .../Handlers/GraphQL/SiteInfoDictionary.cs | 17 ++++ .../GraphQL/SiteInfoDictionaryItem.cs | 17 ++++ 8 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingDictionaryResponse.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PageInfo.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionary.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionaryItem.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingDictionaryResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingDictionaryResponse.cs new file mode 100644 index 0000000..0ae64f1 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingDictionaryResponse.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents a Sitecore sites dictionary collection response. +/// +public class EditingDictionaryResponse +{ + /// + /// Gets or sets the Site for the Editing Layout Response. + /// + public Site? Site { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs index 8a7370b..30a4ca3 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -1,4 +1,5 @@ -using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Microsoft.AspNetCore.Mvc.Formatters; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -11,4 +12,9 @@ public class EditingLayoutQueryResponse /// Gets or sets Item for the Editing Layout Response. ///
public ItemModel? Item { get; set; } + + /// + /// Gets or sets the Site for the Editing Layout Response. + /// + public Site? Site { get; set; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 76436d2..1f53e21 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; @@ -165,15 +165,15 @@ private static void ProcessField(Dictionary updatedFields, { datasource = new { - id = editableField?.MetaData.DataSource.Id, - language = editableField?.MetaData.DataSource.Language, - revision = editableField?.MetaData.DataSource.Revision, - version = editableField?.MetaData.DataSource.Version + id = editableField?.MetaData?.DataSource?.Id, + language = editableField?.MetaData?.DataSource?.Language, + revision = editableField?.MetaData?.DataSource?.Revision, + version = editableField?.MetaData?.DataSource?.Version }, - title = editableField?.MetaData.Title, - fieldId = editableField?.MetaData.FieldId, - fieldType = editableField?.MetaData.FieldType, - rawValue = editableField?.MetaData.RawValue + title = editableField?.MetaData?.Title, + fieldId = editableField?.MetaData?.FieldId, + fieldType = editableField?.MetaData?.FieldType, + rawValue = editableField?.MetaData?.RawValue }; editableField.OpeningChrome = GenerateEditableChrome("field", "open", string.Empty, JsonSerializer.Serialize(openingChromeContent)); @@ -228,12 +228,73 @@ private static string GetRequestArgValue(SitecoreLayoutRequest request, string a return headers[argName].FirstOrDefault() ?? string.Empty; } - private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) + private static async Task GetFullDictionaryInformation(SitecoreLayoutRequest request, string requestLanguage, IGraphQLClient client, GraphQLResponse response) + { + var hasNext = response.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false; + var endCursor = response.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty; + while (hasNext && endCursor != string.Empty) + { + GraphQLRequest dictionaryRequest = BuildEditingDictionaryRequest(request, requestLanguage, endCursor); + + GraphQLResponse dictionaryResponse = await client.SendQueryAsync(dictionaryRequest).ConfigureAwait(false); + response.Data?.Site?.SiteInfo?.Dictionary?.Results.AddRange(dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.Results ?? []); + + hasNext = dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false; + endCursor = dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty; + } + } + + private static GraphQLRequest BuildEditingDictionaryRequest(SitecoreLayoutRequest request, string requestLanguage, string endCursor) + { + return new() + { + Query = @" + query EditingDictionaryQuery( + $siteName: String! + $language: String! + $after: String + $pageSize: Int + ) { + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + ", + OperationName = "EditingDictionaryQuery", + Variables = new + { + language = requestLanguage, + siteName = request.SiteName(), + pageSize = 10, + after = endCursor + } + }; + } + + private static GraphQLRequest BuildEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage) { - GraphQLRequest layoutRequest = new() + return new() { Query = @" - query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $version: String, $after: String, $pageSize: Int = 10) { + query EditingQuery( + $siteName: String!, + $itemId: String!, + $language: String!, + $version: String, + $after: String, + $pageSize: Int + ) { item(path: $itemId, language: $language, version: $version) { rendered } @@ -241,7 +302,7 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve siteInfo(site: $siteName) { dictionary(language: $language, first: $pageSize, after: $after) { results { - key + key value } pageInfo { @@ -260,13 +321,18 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve language = requestLanguage, siteName = request.SiteName(), version = GetRequestArgValue(request, "sc_version"), - pageSize = 50, + pageSize = 10, after = string.Empty } }; + } + private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) + { IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode") == "edit"); - GraphQLResponse response = await client.SendQueryAsync(layoutRequest).ConfigureAwait(false); + GraphQLResponse response = await client.SendQueryAsync(BuildEditingLayoutRequest(request, requestLanguage)).ConfigureAwait(false); + + await GetFullDictionaryInformation(request, requestLanguage, client, response).ConfigureAwait(false); if (logger.IsEnabled(LogLevel.Debug)) { @@ -294,8 +360,7 @@ query EditingQuery($siteName: String!, $itemId: String!, $language: String!, $ve if (response.Errors != null) { - errors.AddRange( - response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + errors.AddRange(response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); } return content; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PageInfo.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PageInfo.cs new file mode 100644 index 0000000..f98d39c --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PageInfo.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents the page info for a Sitecore site. +/// +public class PageInfo +{ + /// + /// Gets or sets the start cursor. + /// + public string EndCursor { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether there are more records to be retrieved. + /// + public bool HasNext { get; set; } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs new file mode 100644 index 0000000..2e738bb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents a Sitecore site. +/// +public class Site +{ + /// + /// Gets or sets the site info. + /// + public SiteInfo? SiteInfo { get; set; } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs new file mode 100644 index 0000000..fedbf41 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs @@ -0,0 +1,12 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents the info for a Sitecore site. +/// +public class SiteInfo +{ + /// + /// Gets or sets the dictionary for a Sitecore Site. + /// + public SiteInfoDictionary? Dictionary { get; set; } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionary.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionary.cs new file mode 100644 index 0000000..485dd1d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionary.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents the dictionary for a Sitecore site. +/// +public class SiteInfoDictionary +{ + /// + /// Gets or sets collection of dictionary items for a Sitecore Site. + /// + public List Results { get; set; } = []; + + /// + /// Gets or sets the PageInfo for the Sitecore Site. + /// + public PageInfo? PageInfo { get; set; } +} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionaryItem.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionaryItem.cs new file mode 100644 index 0000000..053c831 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfoDictionaryItem.cs @@ -0,0 +1,17 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Represents a dictionary item for a Sitecore site. +/// +public class SiteInfoDictionaryItem +{ + /// + /// Gets or sets the key for the dictionary item. + /// + public string Key { get; set; } = string.Empty; + + /// + /// Gets or sets the value for the dictionary item. + /// + public string Value { get; set; } = string.Empty; +} From 5f34722034f3f62ac56c130786f0e6b3abe4a095 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 31 Jan 2025 14:04:32 +1100 Subject: [PATCH 07/38] Fixed failing unit tests --- .../TagHelpers/Fields/DateTagHelper.cs | 2 +- .../TagHelpers/Fields/ImageTagHelper.cs | 2 +- .../TagHelpers/Fields/LinkTagHelper.cs | 2 +- .../TagHelpers/Fields/NumberTagHelper.cs | 2 +- .../TagHelpers/Fields/RichTextTagHelper.cs | 2 +- .../TagHelpers/Fields/TextFieldTagHelper.cs | 31 +++++++++++++------ .../Converter/FieldConverterTests.cs | 12 +++---- .../TagHelpers/Fields/DateTagHelperFixture.cs | 3 +- .../Fields/ImageTagHelperFixture.cs | 3 +- .../TagHelpers/Fields/LinkTagHelperFixture.cs | 3 +- .../Fields/NumberTagHelperFixture.cs | 3 +- .../Fields/TextFieldTagHelperFixture.cs | 7 ++++- 12 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs index 0f24e89..696c7ce 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs @@ -16,7 +16,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute)] public class DateTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs index ced2efc..62446e9 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs @@ -29,7 +29,7 @@ public class ImageTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper private const string VSpaceAttribute = "vspace"; private const string TitleAttribute = "title"; private const string BorderAttribute = "border"; - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs index 58b7747..64f0d6c 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs @@ -53,7 +53,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) HyperLinkField? field = LinkModel ?? For?.Model as HyperLinkField; bool outputEditableMarkup = Editable && - ((!string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field.EditableMarkupLast)) || (field.OpeningChrome != null && field.ClosingChrome != null)); + ((!string.IsNullOrEmpty(field?.EditableMarkupFirst) && !string.IsNullOrWhiteSpace(field?.EditableMarkupLast)) || (field?.OpeningChrome != null && field?.ClosingChrome != null)); if (field == null || (string.IsNullOrWhiteSpace(field.Value.Href) && !outputEditableMarkup)) { diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs index 5d2ec75..513eabf 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs @@ -15,7 +15,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute)] public class NumberTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs index 3de128a..a4006ca 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs @@ -16,7 +16,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute)] public class RichTextTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs index 3756a39..6c9938e 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs @@ -1,7 +1,7 @@ -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; -using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; @@ -13,7 +13,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] public partial class TextFieldTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -46,26 +46,37 @@ public override void Process(TagHelperContext context, TagHelperOutput output) bool outputEditableMarkup = Editable && !string.IsNullOrEmpty(field.EditableMarkup); string value = outputEditableMarkup ? field.EditableMarkup : field.Value; + string html = string.Empty; + bool isHtml = false; if (Editable && field.OpeningChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); - output.Content.AppendHtml("
"); + html += chromeRenderer.Render(field.OpeningChrome); + html += "
"; } if (outputEditableMarkup || (ConvertNewLines && NewLineRegex().IsMatch(value))) { - value = NewLineRegex().Replace(value, "
"); - output.Content.AppendHtml(value); + html += NewLineRegex().Replace(value, "
"); + isHtml = true; } else { - output.Content.Append(value); + html += value; } if (Editable && field.ClosingChrome != null) { - output.Content.AppendHtml("
"); - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + html += "
"; + html += chromeRenderer.Render(field.ClosingChrome); + } + + if (isHtml) + { + output.Content.SetHtmlContent(html); + } + else + { + output.Content.SetContent(html); } } diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs index de84120..0efa66f 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs @@ -17,12 +17,12 @@ public class FieldConverterTests public static TheoryData Fields => new() { - { new TextField("Test"), """{"editable":"","value":"Test"}""" }, - { new RichTextField("Test Noencoding", false), """{"editable":"","value":"Test Noencoding"}""" }, - { new RichTextField("Test%20Encoded"), """{"editable":"","value":"Test Encoded"}""" }, - { new CheckboxField(true), """{"editable":"","value":true}""" }, - { new CheckboxField(false), """{"editable":""}""" }, - { new ImageField(new Image { Alt = "Alt Text", Border = 1, Class = "styleclass", HSpace = 1, Height = 100, Src = "https://image.com/test.jpg", Title = "Title", VSpace = 1, Width = 100 }), """{"editable":"","value":{"src":"https://image.com/test.jpg","alt":"Alt Text","height":"100","width":"100","title":"Title","hSpace":"1","vSpace":"1","border":"1","class":"styleclass"}}""" } + { new TextField("Test"), """{"editable":"","Id":"","value":"Test"}""" }, + { new RichTextField("Test Noencoding", false), """{"editable":"","Id":"","value":"Test Noencoding"}""" }, + { new RichTextField("Test%20Encoded"), """{"editable":"","Id":"","value":"Test Encoded"}""" }, + { new CheckboxField(true), """{"editable":"","Id":"","value":true}""" }, + { new CheckboxField(false), """{"editable":"","Id":""}""" }, + { new ImageField(new Image { Alt = "Alt Text", Border = 1, Class = "styleclass", HSpace = 1, Height = 100, Src = "https://image.com/test.jpg", Title = "Title", VSpace = 1, Width = 100 }), """{"editable":"","Id":"","value":{"src":"https://image.com/test.jpg","alt":"Alt Text","height":"100","width":"100","title":"Title","hSpace":"1","vSpace":"1","border":"1","class":"styleclass"}}""" } }; [Fact] diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs index 33c2a2c..032654b 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -35,7 +36,7 @@ public class DateTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new DateTagHelper()); + f.Register(() => new DateTagHelper(new EditableChromeRenderer())); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs index ab8d12e..3df6ead 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs @@ -13,6 +13,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -38,7 +39,7 @@ public class ImageTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new ImageTagHelper()); + f.Register(() => new ImageTagHelper(new EditableChromeRenderer())); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs index 49cb967..3f3a208 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -38,7 +39,7 @@ public class LinkTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new LinkTagHelper()); + f.Register(() => new LinkTagHelper(new EditableChromeRenderer())); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs index c577671..2feed2b 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -37,7 +38,7 @@ public class NumberTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new NumberTagHelper()); + f.Register(() => new NumberTagHelper(new EditableChromeRenderer())); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs index 1e7a04e..90d746d 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs @@ -13,6 +13,7 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -25,10 +26,14 @@ public class TextFieldTagHelperFixture private const string TestHtml = "

This is the test text

"; private static readonly string TestMultilineText = $"

This is the test text {Environment.NewLine} with line endings.

"; + private static EditableChromeRenderer _editableChromeRenderer = null!; + // ReSharper disable once UnusedMember.Global - Used by testing framework [ExcludeFromCodeCoverage] public static Action AutoSetup => f => { + _editableChromeRenderer = f.Freeze(); + TagHelperContext tagHelperContext = new( [], new Dictionary(), @@ -40,7 +45,7 @@ public class TextFieldTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new TextFieldTagHelper()); + f.Register(() => new TextFieldTagHelper(_editableChromeRenderer)); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); From 5494e8e4ce44d8dbdfeecc2f9331a33646f7a774 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 3 Feb 2025 16:47:26 +1100 Subject: [PATCH 08/38] Added UnitTests for EditingScriptsTagHelper --- Sitecore.AspNetCore.SDK.sln | 7 + .../Response/CanvasState.cs | 4 +- .../TagHelpers/EditingScriptsTagHelper.cs | 14 +- ...Sitecore.AspNetCore.SDK.Pages.Tests.csproj | 12 + .../EditingScriptsTagHelperFixture.cs | 220 ++++++++++++++++++ 5 files changed, 244 insertions(+), 13 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Sitecore.AspNetCore.SDK.Pages.Tests.csproj create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/TagHelpers/EditingScriptsTagHelperFixture.cs diff --git a/Sitecore.AspNetCore.SDK.sln b/Sitecore.AspNetCore.SDK.sln index 7fc2785..bb970c2 100644 --- a/Sitecore.AspNetCore.SDK.sln +++ b/Sitecore.AspNetCore.SDK.sln @@ -114,6 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sitecore.AspNetCore.SDK.Pages", "src\Sitecore.AspNetCore.SDK.Pages\Sitecore.AspNetCore.SDK.Pages.csproj", "{F33DC6F7-83F8-41CE-852C-8279D1266139}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sitecore.AspNetCore.SDK.Pages.Tests", "tests\Sitecore.AspNetCore.SDK.Pages.Tests\Sitecore.AspNetCore.SDK.Pages.Tests.csproj", "{55601B5C-5D9C-66E5-801D-E5D5EA0E29D6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -192,6 +194,10 @@ Global {F33DC6F7-83F8-41CE-852C-8279D1266139}.Debug|Any CPU.Build.0 = Debug|Any CPU {F33DC6F7-83F8-41CE-852C-8279D1266139}.Release|Any CPU.ActiveCfg = Release|Any CPU {F33DC6F7-83F8-41CE-852C-8279D1266139}.Release|Any CPU.Build.0 = Release|Any CPU + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -228,6 +234,7 @@ Global {1706E43D-AC19-4FBB-9BFB-18A8B195580A} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} {24CCC156-046B-4600-9DB0-FC3269A18747} = {5FE82369-DEF2-4136-B74F-6E86DB91050E} {F33DC6F7-83F8-41CE-852C-8279D1266139} = {75482B5D-21E2-4DBE-BE78-657ECF0D409F} + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E4F7126-B772-42CB-8F90-93B221ED0A72} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs index 7615a4a..0e07bcf 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs @@ -46,6 +46,6 @@ public class CanvasState /// /// Gets or sets the current id of the Varient being edited. /// - [JsonPropertyName("varient")] - public string? Varient { get; set; } + [JsonPropertyName("variant")] + public string? Variant { get; set; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs index d2869e9..df5cfdf 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -1,10 +1,8 @@ -using System.Resources; -using System.Text.Json; +using System.Text.Json; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; using Sitecore.AspNetCore.SDK.Pages.Response; -using Sitecore.AspNetCore.SDK.RenderingEngine; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; @@ -34,16 +32,10 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu if (renderingContext?.Response?.Content?.Sitecore?.Context?.IsEditing ?? false) { - JsonDocument doc = JsonDocument.Parse(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); - if (doc == null) - { - throw new NullReferenceException("EditingScriptsTagHelper: Unable to process ContextRawData"); - } - EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); if (editingContext == null) { - return; + throw new NullReferenceException("EditingScriptsTagHelper: Unable to process ContextRawData"); } foreach (string script in editingContext.ClientScripts ?? []) @@ -60,7 +52,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu ""language"":""{editingContext?.ClientData?.CanvasState?.Language}"", ""deviceId"":""{editingContext?.ClientData?.CanvasState?.DeviceId}"", ""pageMode"":""{editingContext?.ClientData?.CanvasState?.PageMode}"", - ""variant"":""{editingContext?.ClientData?.CanvasState?.Varient}"" + ""variant"":""{editingContext?.ClientData?.CanvasState?.Variant}"" }} "; diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Sitecore.AspNetCore.SDK.Pages.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Sitecore.AspNetCore.SDK.Pages.Tests.csproj new file mode 100644 index 0000000..6222bc7 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Sitecore.AspNetCore.SDK.Pages.Tests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/TagHelpers/EditingScriptsTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/TagHelpers/EditingScriptsTagHelperFixture.cs new file mode 100644 index 0000000..679a267 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/TagHelpers/EditingScriptsTagHelperFixture.cs @@ -0,0 +1,220 @@ +using System.Diagnostics.CodeAnalysis; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.Pages.TagHelpers; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.TagHelpers; + +public class EditingScriptsTagHelperFixture +{ + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + + FeatureCollection features = new(); + features[typeof(ISitecoreRenderingContext)] = Substitute.For(); + + viewContext.HttpContext.Features.Returns(features); + f.Inject(viewContext); + + TagHelperOutput tagHelperOutput = new("test", [], (_, _) => + { + DefaultTagHelperContent tagHelperContent = new(); + tagHelperContent.SetHtmlContent(string.Empty); + return Task.FromResult(tagHelperContent); + }); + + f.Inject(tagHelperOutput); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_NoSitecoreContenxt_ExceptionIsThrown(EditingScriptsTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput) + { + // Arrange + ViewContext viewContext = new() + { + HttpContext = Substitute.For() + }; + FeatureCollection features = new(); + viewContext.HttpContext.Features.Returns(features); + sut.ViewContext = viewContext; + + // Act + Func act = async () => await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_NotInEditingMode_OutputIsEmpty(EditingScriptsTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput, ViewContext viewContext) + { + // Arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = false + } + } + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + + // Act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().BeEmpty(); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_IsInEditingMode_ClientScriptsAreOutput(EditingScriptsTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput, ViewContext viewContext) + { + // arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + }, + ContextRawData = "{\"clientScripts\":[\"/assets/js/script1.js\", \"/assets/js/script2.js\", \"/assets/js/script3.js\"]}" + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + + // act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // assert + tagHelperOutput.Content.GetContent().Should().Contain(""); + tagHelperOutput.Content.GetContent().Should().Contain(""); + tagHelperOutput.Content.GetContent().Should().Contain(""); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_IsInEditingMode_ItemDataScriptTagIsOutput(EditingScriptsTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput, ViewContext viewContext) + { + // arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + }, + ContextRawData = "{\"clientData\":{\"hrz-canvas-state\": {\"itemId\": \"itemId-1234\",\"itemVersion\": 1,\"siteName\": \"siteName_1234\",\"language\": \"en\",\"deviceId\": \"device_1234\",\"pageMode\": \"NORMAL\",\"variant\": \"variant_1234\"}}}" + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + + // act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // assert + string expectedItemDataScriptTag = $@" + + "; + tagHelperOutput.Content.GetContent().Should().Contain(expectedItemDataScriptTag); + } + + [Theory] + [AutoNSubstituteData] + public async Task ProcessAsync_IsInEditingMode_CanvasVerificationTokenIsOutput(EditingScriptsTagHelper sut, TagHelperContext tagHelperContext, TagHelperOutput tagHelperOutput, ViewContext viewContext) + { + // arrange + SitecoreRenderingContext context = new() + { + Response = new SitecoreLayoutResponse([]) + { + Content = new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Context = new Context + { + IsEditing = true + } + }, + ContextRawData = "{\"clientData\":{\"hrz-canvas-verification-token\":\"token_1234\"}}" + } + } + }; + + viewContext.HttpContext.SetSitecoreRenderingContext(context); + sut.ViewContext = viewContext; + + // act + await sut.ProcessAsync(tagHelperContext, tagHelperOutput); + + // assert + tagHelperOutput.Content.GetContent().Should().Contain(""); + } +} \ No newline at end of file From d007e5b58e83808c709782f09bc277481bee4c12 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 4 Feb 2025 11:49:45 +1100 Subject: [PATCH 09/38] Added UnitTests for Pages GQL Factory, and RenderingEngine TagHelpers --- .../GraphQL/GraphQLClientFactory.cs | 2 +- .../PagesAppConfigurationExtensionsFixture.cs | 19 +++++ .../GraphQL/GraphQLClientFactoryFixture.cs | 79 +++++++++++++++++++ .../TagHelpers/Fields/DateTagHelperFixture.cs | 29 +++++++ .../Fields/ImageTagHelperFixture.cs | 27 +++++++ .../TagHelpers/Fields/LinkTagHelperFixture.cs | 28 +++++++ .../Fields/NumberTagHelperFixture.cs | 28 +++++++ .../Fields/RichTextTagHelperFixture.cs | 29 +++++++ .../Fields/TextFieldTagHelperFixture.cs | 28 +++++++ 9 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs index 9d23d29..3e4f0a7 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs @@ -12,7 +12,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.GraphQL; public class GraphQLClientFactory(string contextId) : IGraphQLClientFactory { - private readonly string contextId = contextId; + private readonly string contextId = contextId ?? throw new ArgumentNullException(nameof(contextId)); /// public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, bool editMode) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs new file mode 100644 index 0000000..e7aa3ea --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.Pages.Extensions; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Extensions +{ + public class PagesAppConfigurationExtensionsFixture + { + [Fact] + public void UseSitecorePages_AppIsNull_ExceptionIsThrown() + { + // Act + Action action = () => PagesAppConfigurationExtensions.UseSitecorePages(null!); + + // Assert + action.Should().Throw(); + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs new file mode 100644 index 0000000..a36db2c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs @@ -0,0 +1,79 @@ +using AutoFixture.Idioms; +using FluentAssertions; +using GraphQL.Client.Http; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.Pages.GraphQL; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.GraphQL +{ + public class GraphQLClientFactoryFixture + { + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public void GenerateClient_NullUriDefaultsToEdgePlatformUri(GraphQLClientFactory sut) + { + // Act + var result = sut.GenerateClient(null, string.Empty, false); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1"); + } + + [Theory] + [AutoNSubstituteData] + public void GenerateClient_OverriddenUriUsedInClient(GraphQLClientFactory sut) + { + // Act + var result = sut.GenerateClient(new Uri("https://some.domain.com"), string.Empty, false); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("https://some.domain.com"); + } + + [Theory] + [AutoNSubstituteData] + public void GenerateClient_ContextIdIsAppendedToClientUri(GraphQLClientFactory sut) + { + // Arrange + sut = new GraphQLClientFactory("1234"); + + // Act + var result = sut.GenerateClient(null, string.Empty, false); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("sitecoreContextId=1234"); + } + + [Theory] + [AutoNSubstituteData] + public void GenerateClient_CorrectParamsAreSetWhenGeneratingClient(GraphQLClientFactory sut) + { + // Arrange + sut = new GraphQLClientFactory("1234"); + + // Act + var result = sut.GenerateClient(null, "layout_kid_1234", true); + + // Assert + result.Should().NotBeNull(); + result.Should().BeOfType(); + result.As().HttpClient.DefaultRequestHeaders.GetValues("sc_layoutKind").Should().Equal(["layout_kid_1234"]); + result.As().HttpClient.DefaultRequestHeaders.GetValues("sc_editmode").Should().Equal([true.ToString()]); + } + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs index 032654b..1ba472b 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs @@ -338,6 +338,35 @@ public void Process_ScDateWithAspDataAttributeTagWithEditableFieldAndEditableSet } #endregion + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + DateTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.DateHtmlTag; + DateField testField = new(_date) + { + EditableMarkup = Editable, + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.DateModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + tagHelperOutput.Content.GetContent().Should().Contain(Editable); + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static ModelExpression GetModelExpression(Field model) { DefaultModelMetadata? modelMetadata = Substitute.For( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs index 3df6ead..e9d9baf 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/ImageTagHelperFixture.cs @@ -712,6 +712,33 @@ public void Process_ImgTagAspImageAttributeWithAllAttributes_AddsAllAttributes( } #endregion + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + ImageTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + ImageField testField = new(new Image { Alt = "Sitecore Logo", Src = "/sitecore/shell/-/media/styleguide/data/media/img/sc_logo.png?iar=0" }) + { + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.ImageModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static ModelExpression GetModelExpression(Field model) { DefaultModelMetadata? modelMetadata = Substitute.For( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs index 3f3a208..7ead796 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs @@ -784,6 +784,34 @@ public void Process_AnchorLinkTagWithAnchor_AddsAnchorToHrefAttribute( } #endregion + + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + LinkTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.ImageHtmlTag; + HyperLinkField testField = new(_hyperLink) + { + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.LinkModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static ModelExpression GetModelExpression(Field model) { DefaultModelMetadata? modelMetadata = Substitute.For( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs index 2feed2b..48d4943 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/NumberTagHelperFixture.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Properties; using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -302,6 +303,33 @@ public void Process_ScNumberWithAspNumberAttributeTagWithCultureInfo_GeneratesCo } #endregion + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + NumberTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.NumberHtmlTag; + NumberField testField = new(Number1) + { + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.NumberModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static ModelExpression GetModelExpression(Field model) { DefaultModelMetadata? modelMetadata = Substitute.For( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs index 2fbb4e4..aaba593 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model.Fields; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; using Xunit; @@ -707,6 +708,34 @@ public void Process_DivTagWithEditableFieldAndEditableSetToFalseAndTextAttribute #endregion + + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + RichTextTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + RichTextField testField = new(TestHtml, false) + { + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.TextModel = testField; + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static ModelExpression GetModelExpression(Field model) { DefaultModelMetadata? modelMetadata = Substitute.For( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs index 90d746d..75cbe61 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs @@ -189,6 +189,34 @@ public void Process_EditableFieldAndEditableSetToFalse_GeneratesCorrectOutput(Te tagHelperOutput.Content.GetContent().Should().Be(TestText); } + + [Theory] + [AutoNSubstituteData] + public void Process_RenderingChromesAreNotNull_ChromesAreOutput( + TagHelperContext tagHelperContext, + TagHelperOutput tagHelperOutput) + { + // Arrange + IEditableChromeRenderer chromeRenderer = Substitute.For(); + TextFieldTagHelper sut = new(chromeRenderer); + EditableChrome openingChrome = Substitute.For(); + EditableChrome closingChrome = Substitute.For(); + tagHelperOutput.TagName = RenderingEngineConstants.SitecoreTagHelpers.RichTextHtmlTag; + TextField testField = new(TestText) + { + OpeningChrome = openingChrome, + ClosingChrome = closingChrome + }; + sut.For = GetModelExpression(testField); + + // Act + sut.Process(tagHelperContext, tagHelperOutput); + + // Assert + chromeRenderer.Received().Render(openingChrome); + chromeRenderer.Received().Render(closingChrome); + } + private static IEnumerable GetModelExpressionTestData() { yield return [null!, string.Empty]; From b92f31d84c8e1673fdcee09b42fd758555591c48 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 5 Feb 2025 12:49:21 +1100 Subject: [PATCH 10/38] ComponentName is now persisted in rendering collection, to be returned in editing config request. Updated related tests --- .../Middleware/PageSetupMiddleware.cs | 38 ++++--------------- .../RenderingEngineOptionsExtensions.cs | 5 ++- .../Rendering/ComponentRendererDescriptor.cs | 8 +++- .../Rendering/LoggingComponentRenderer.cs | 3 +- .../Rendering/PartialViewComponentRenderer.cs | 3 +- .../ViewComponentComponentRenderer.cs | 3 +- ...RenderingEngineOptionsExtensionsFixture.cs | 3 +- .../ComponentRendererDescriptorFixture.cs | 5 ++- .../ComponentRendererFactoryFixture.cs | 2 +- .../TagHelpers/PlaceholderTagHelperFixture.cs | 2 +- 10 files changed, 31 insertions(+), 41 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs index 337276d..b182d76 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs @@ -5,6 +5,7 @@ using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Models; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; namespace Sitecore.AspNetCore.SDK.Pages.Middleware; @@ -100,39 +101,16 @@ private async Task BuildResponse(HttpResponse httpResponse) httpResponse.StatusCode = StatusCodes.Status200OK; httpResponse.ContentType = "application/json"; - // TODO: change the Components list to not be hardcoded! - string responseBody = @" - { + string componentNames = string.Join(",\r\n", renderingEngineOptions.RendererRegistry.Select(x => $"\"{x.Value.ComponentName}\"")); + string responseBody = $@" + {{ ""components"": [ - ""Title"", - ""Container"", - ""ColumnSplitter"", - ""RowSplitter"", - ""PageContent"", - ""RichText"", - ""Promo"", - ""LinkList"", - ""Image"", - ""PartialDesignDynamicPlaceholder"", - ""Navigation"" + {componentNames} ], - ""packages"": { - ""@sitecore/byoc"": ""0.2.15"", - ""@sitecore/components"": ""1.1.10"", - ""@sitecore/engage"": ""1.4.3"", - ""@sitecore-cloudsdk/core"": ""0.3.1"", - ""@sitecore-cloudsdk/events"": ""0.3.1"", - ""@sitecore-cloudsdk/personalize"": ""0.3.1"", - ""@sitecore-cloudsdk/utils"": ""0.3.1"", - ""@sitecore-feaas/clientside"": ""0.5.18"", - ""@sitecore-jss/sitecore-jss"": ""22.1.3"", - ""@sitecore-jss/sitecore-jss-cli"": ""22.1.3"", - ""@sitecore-jss/sitecore-jss-dev-tools"": ""22.1.3"", - ""@sitecore-jss/sitecore-jss-nextjs"": ""22.1.3"", - ""@sitecore-jss/sitecore-jss-react"": ""22.1.3"" - }, + ""packages"": {{ + }}, ""editMode"": ""metadata"" - } + }} "; await httpResponse.WriteAsync(responseBody); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs index ea0c681..bbe9951 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs @@ -223,7 +223,8 @@ public static RenderingEngineOptions AddModelBoundView( sp => ActivatorUtilities.CreateInstance>( sp, RenderingEngineConstants.SitecoreViewComponents.DefaultSitecoreViewComponentName, - viewName)); + viewName), + viewName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); @@ -242,7 +243,7 @@ public static RenderingEngineOptions AddDefaultComponentRenderer( { ArgumentNullException.ThrowIfNull(options); - ComponentRendererDescriptor descriptor = new(_ => true, services => ActivatorUtilities.CreateInstance(services)); + ComponentRendererDescriptor descriptor = new(_ => true, services => ActivatorUtilities.CreateInstance(services), "defaultComponent"); options.DefaultRenderer = descriptor; return options; diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs index cf6e541..335eaea 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs @@ -10,7 +10,8 @@ /// The factory method to create a new instance of the . public class ComponentRendererDescriptor( Predicate match, - Func factory) + Func factory, + string componentName) { private readonly Func _factory = factory ?? throw new ArgumentNullException(nameof(factory)); private readonly object _lock = new(); @@ -22,6 +23,11 @@ public class ComponentRendererDescriptor( ///
public Predicate Match { get; } = match ?? throw new ArgumentNullException(nameof(match)); + /// + /// Gets the name of the component. + /// + public string ComponentName { get; } = componentName; + /// /// Gets an instance of an , creating one if it has not yet been instantiated. /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs index 9b48514..f6ddf81 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs @@ -31,7 +31,8 @@ public static ComponentRendererDescriptor Describe(Predicate match) return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp)); + sp => ActivatorUtilities.CreateInstance(sp), + string.Empty); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs index 1fabce1..777ebf5 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs @@ -35,7 +35,8 @@ public static ComponentRendererDescriptor Describe(Predicate match, stri ArgumentException.ThrowIfNullOrWhiteSpace(locator); return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp, locator)); + sp => ActivatorUtilities.CreateInstance(sp, locator), + locator); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs index 73dc497..8c2e97f 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs @@ -37,7 +37,8 @@ public static ComponentRendererDescriptor Describe(Predicate match, stri return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp, locator)); + sp => ActivatorUtilities.CreateInstance(sp, locator), + string.Empty); } /// diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs index 7e2680b..df54f1e 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/RenderingEngineOptionsExtensionsFixture.cs @@ -224,7 +224,8 @@ public void AddModelBoundViewOfTModel_ValidParameters_OptionsRendererRegistryCon public void AddModelBoundViewOfTModel_ValidParametersRendererRegistryIsNotEmpty_NewComponentRendererDescriptorIsAddedToTheEndOfTheList(RenderingEngineOptions options, string viewComponentName) { // Arrange - ComponentRendererDescriptor initialDescriptor = new(name => name == "InitialComponent", _ => null!); + string componentName = "InitialComponent"; + ComponentRendererDescriptor initialDescriptor = new(name => name == componentName, _ => null!, componentName); options.RendererRegistry.Add(0, initialDescriptor); // Act diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs index 4f9a297..b2817e8 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererDescriptorFixture.cs @@ -30,7 +30,7 @@ public void Ctor_IsGuarded() { // Arrange Func act = - () => new ComponentRendererDescriptor(null!, null!); + () => new ComponentRendererDescriptor(null!, null!, string.Empty); // Act & Assert act.Should().Throw(); @@ -41,7 +41,8 @@ public void Ctor_IsGuarded() public void GetOrCreate_ServiceProviderContainsRendererType_ReturnsRendererType(IServiceProvider services, IComponentRenderer renderer) { // Arrange - ComponentRendererDescriptor sut = new(name => name == "Test", _ => renderer); + string componentName = "Test"; + ComponentRendererDescriptor sut = new(name => name == componentName, _ => renderer, componentName); // Act IComponentRenderer result = sut.GetOrCreate(services); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs index 6316877..2a78741 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ComponentRendererFactoryFixture.cs @@ -30,7 +30,7 @@ public class ComponentRendererFactoryFixture { RendererRegistry = new SortedList { - { 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer) } + { 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer, TestComponentName) } } }; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs index b87fe29..cd9724f 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs @@ -63,7 +63,7 @@ public class PlaceholderTagHelperFixture RendererRegistry = new SortedList { { - 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer) + 0, new ComponentRendererDescriptor(name => name == TestComponentName, _ => componentRenderer, TestComponentName) } } }; From 01a92282294299327abe32cf2f2aa670e9b849f2 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 5 Feb 2025 15:55:40 +1100 Subject: [PATCH 11/38] Added UnitTests for Pages Config Middleware --- .../Middleware/PageSetupMiddleware.cs | 43 +++-- .../Middleware/PageSetupMiddlewareFixture.cs | 165 ++++++++++++++++++ 2 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs index b182d76..c3ffa37 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs @@ -6,6 +6,8 @@ using Sitecore.AspNetCore.SDK.Pages.Models; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using System.Net.Http; +using System.Text.Json; namespace Sitecore.AspNetCore.SDK.Pages.Middleware; @@ -97,23 +99,36 @@ private async Task BuildResponse(HttpResponse httpResponse) httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; - httpResponse.StatusCode = StatusCodes.Status200OK; httpResponse.ContentType = "application/json"; - string componentNames = string.Join(",\r\n", renderingEngineOptions.RendererRegistry.Select(x => $"\"{x.Value.ComponentName}\"")); - string responseBody = $@" - {{ - ""components"": [ - {componentNames} - ], - ""packages"": {{ - }}, - ""editMode"": ""metadata"" - }} - "; - - await httpResponse.WriteAsync(responseBody); + Stream realResponseStream = httpResponse.Body; + try + { + MemoryStream tmpResponseBuffer = new(); + + httpResponse.Body = tmpResponseBuffer; + + tmpResponseBuffer.Position = 0; + string componentNames = string.Join(",\r\n", renderingEngineOptions.RendererRegistry.Select(x => $"\"{x.Value.ComponentName}\"")); + string responseBody = $@" + {{ + ""components"": [ + {componentNames} + ], + ""packages"": {{ + }}, + ""editMode"": ""metadata"" + }} + "; + + await using StreamWriter realResponseWriter = new(realResponseStream); + await realResponseWriter.WriteAsync(responseBody).ConfigureAwait(false); + } + finally + { + httpResponse.Body = realResponseStream; + } } private bool IsValidEditingSecret(HttpRequest httpRequest) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs new file mode 100644 index 0000000..f6a35da --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs @@ -0,0 +1,165 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Middleware +{ + public class PageSetupMiddlewareFixture + { + private const string ValidConfigEndpoint = "/api/editing/config"; + private const string ValidRenderEndpoint = "/api/editing/render"; + private const string ValidEditingOrigin = "http://some.editing.domain"; + private const string ValidOrigins = "http://some.origin.domain"; + private const string ValidEditingSecret = "editing_secret_1234"; + + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + RequestDelegate requestDelegate = Substitute.For(); + f.Inject(requestDelegate); + + IOptions pagesOptions = Substitute.For>(); + PagesOptions PagesOptionsValues = new PagesOptions + { + ConfigEndpoint = ValidConfigEndpoint, + RenderEndpoint = ValidRenderEndpoint, + ValidEditingOrigin = ValidEditingOrigin, + ValidOrigins = ValidOrigins, + EditingSecret = ValidEditingSecret + }; + pagesOptions.Value.Returns(PagesOptionsValues); + f.Inject(pagesOptions); + + ILogger logger = Substitute.For>(); + f.Inject(logger); + + IOptions renderingEngineOptions = Substitute.For>(); + string componentName = "TestComponent"; + ComponentRendererDescriptor componentRendererDescriptor = new(name => name == componentName, _ => null!, componentName); + RenderingEngineOptions renderingEngineOptionsValues = new RenderingEngineOptions + { + RendererRegistry = new SortedList + { + { 1, componentRendererDescriptor } + } + + }; + renderingEngineOptions.Value.Returns(renderingEngineOptionsValues); + f.Inject(renderingEngineOptions); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_RequestIsntConfigOrRender_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + HttpRequest httpRequest = Substitute.For(); + httpRequest.Method.Returns("Post"); + httpContext.Request.Returns(httpRequest); + + // Act + await sut.Invoke(httpContext); + + // Assert + await requestDelegate.Received()(httpContext); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_ConfigRequest_InvalidEditingSecret_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + httpContext.Request.Method.Returns("GET"); + httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); + httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues("incorrect_secret_value") } })); + + // Act + await sut.Invoke(httpContext); + + // Assert + logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Secret Value"), null, Arg.Any>()); + await requestDelegate.Received()(httpContext); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_ConfigRequest_InvalidEditingOrigin_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + httpContext.Request.Method.Returns("GET"); + httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); + httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues(ValidEditingSecret) } })); + httpContext.Request.Headers.Returns(new HeaderDictionary(new Dictionary { { "Origin", new StringValues("http://an.invalid.origin.domain") } })); + + // Act + await sut.Invoke(httpContext); + + // Assert + logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Origin"), null, Arg.Any>()); + await requestDelegate.Received()(httpContext); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_ConfigRequest_ValidResponseReturned(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + httpContext.Request.Method.Returns("GET"); + httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); + httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues(ValidEditingSecret) } })); + httpContext.Request.Headers.Returns(new HeaderDictionary(new Dictionary { { "Origin", new StringValues(ValidEditingOrigin) } })); + MemoryStream memoryStream = new(); + httpContext.Response.Body = memoryStream; + + // Act + await sut.Invoke(httpContext); + + // Assert + logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Config request"), null, Arg.Any>()); + + byte[] contents = memoryStream.ToArray(); + string returnedBody = Encoding.UTF8.GetString(contents); + string expectedResponse = "\r\n {\r\n \"components\": [\r\n \"TestComponent\"\r\n ],\r\n \"packages\": {\r\n },\r\n \"editMode\": \"metadata\"\r\n }\r\n "; + Assert.Equal(expectedResponse, returnedBody); + + httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); + httpContext.Response.Headers.AccessControlAllowOrigin.Should().Equal(ValidEditingOrigin); + httpContext.Response.Headers.AccessControlAllowMethods.Should().Equal("GET, POST, OPTIONS, PUT, PATCH, DELETE"); + httpContext.Response.StatusCode.Should().Be(StatusCodes.Status200OK); + httpContext.Response.ContentType.Should().Be("application/json"); + + await requestDelegate.DidNotReceive()(httpContext); + } + } +} From 5be1780c8dfa2d0a1d82b14f15aad575ab292460 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 5 Feb 2025 17:47:30 +1100 Subject: [PATCH 12/38] Added Unit Tests for Pages Setup Render Middleware call --- .../Middleware/PageSetupMiddlewareFixture.cs | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs index f6a35da..dd5466f 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs @@ -7,11 +7,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; using NSubstitute; using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; -using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Middleware; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; @@ -161,5 +159,74 @@ public async Task Invoke_ConfigRequest_ValidResponseReturned(RequestDelegate req await requestDelegate.DidNotReceive()(httpContext); } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_RenderRequest_InvalidEditingSecret_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + httpContext.Request.Method.Returns("GET"); + httpContext.Request.Path.Returns(new PathString(ValidRenderEndpoint)); + httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues("incorrect_secret_value") } })); + + // Act + await sut.Invoke(httpContext); + + // Assert + logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Secret Value"), null, Arg.Any>()); + await requestDelegate.Received()(httpContext); + } + + [Theory] + [AutoNSubstituteData] + public async Task Invoke_RenderRequest_ValidResponseReturned(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + string expectedRoute = "test_route"; + string expectedMode = "test_mode"; + string expectedItemId = "test_item_id"; + string expectedVersion = "test_version"; + string expectedLanguage = "test_lang"; + string expectedSite = "test_site"; + string expectedLayoutKind = "test_layoutKind"; + string expectedTenantId = "test_tenant_id"; + PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); + HttpContext httpContext = Substitute.For(); + httpContext.Request.Method.Returns("GET"); + httpContext.Request.Path.Returns(new PathString(ValidRenderEndpoint)); + httpContext.Request.Headers.Returns(new HeaderDictionary(new Dictionary { { "Origin", new StringValues(ValidEditingOrigin) } })); + httpContext.Request.Query.Returns(new QueryCollection( + new Dictionary + { + { "secret", new StringValues(ValidEditingSecret) }, + { "sc_itemid", new StringValues(expectedItemId) }, + { "sc_lang", new StringValues(expectedLanguage) }, + { "sc_layoutKind", new StringValues(expectedLayoutKind) }, + { "mode", new StringValues(expectedMode) }, + { "route", new StringValues(expectedRoute) }, + { "sc_site", new StringValues(expectedSite) }, + { "sc_version", new StringValues(expectedVersion) }, + { "tenant_id", new StringValues(expectedTenantId) } + })); + + HttpResponse httpResponse = Substitute.For(); + httpContext.Response.Returns(httpResponse); + MemoryStream memoryStream = new(); + httpResponse.Body = memoryStream; + + // Act + await sut.Invoke(httpContext); + + // Assert + logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Render request"), null, Arg.Any>()); + + httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); + string validRedirectString = $"{expectedRoute}?mode={expectedMode}&sc_itemid={expectedItemId}&sc_version={expectedVersion}&sc_lang={expectedLanguage}&sc_site={expectedSite}&sc_layoutKind={expectedLayoutKind}&secret={ValidEditingSecret}&tenant_id={expectedTenantId}&route={expectedRoute}"; + httpResponse.ReceivedWithAnyArgs().Redirect(validRedirectString, permanent: false); + + await requestDelegate.DidNotReceive()(httpContext); + } } } From 4ea37ae96013c6406b9881b88a420bf290aa6746 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 10 Feb 2025 11:45:07 +1100 Subject: [PATCH 13/38] Added UniTests for GraphQLEditingServiceHandler --- .../GraphQL/GraphQLEditingServiceHandler.cs | 6 +- .../GraphQLEditingServiceHandlerFixture.cs | 166 ++++++++++++++++++ 2 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 1f53e21..e29139c 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -26,9 +26,9 @@ public class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, ILogger logger) : ILayoutRequestHandler { - private readonly IGraphQLClientFactory clientFactory = clientFactory; - private readonly ISitecoreLayoutSerializer serializer = serializer; - private readonly ILogger logger = logger; + private readonly IGraphQLClientFactory clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + private readonly ISitecoreLayoutSerializer serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// public async Task Request(SitecoreLayoutRequest request, string handlerName) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs new file mode 100644 index 0000000..2e3e931 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -0,0 +1,166 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using AutoFixture; +using AutoFixture.Idioms; +using Castle.Core.Logging; +using FluentAssertions; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.Pages.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL +{ + public class GraphQLEditingServiceHandlerFixture + { + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + IGraphQLClient client = Substitute.For(); + IGraphQLClientFactory clientFactory = Substitute.For(); + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement + }, + Site = new Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + PageInfo = new PageInfo + { + HasNext = false + } + } + } + } + } + }); + + f.Inject(clientFactory); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_RequestParamIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request(null!, string.Empty); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_HandlerNameIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request([], null!); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_HandlerNameIsEmptyString_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request([], string.Empty); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_NotValidEditingRequest_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Arrange + SitecoreLayoutRequest request = []; + + // Act + Func act = async () => { await sut.Request(request, "editingHandler"); }; + + // Assert + await act.Should().ThrowAsync().WithMessage("GraphQLEditingServiceHandler: Error attempting to process non-editing request"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Arrange + SitecoreLayoutRequest request = new SitecoreLayoutRequest + { + { + "sc_request_headers_key" , new Dictionary() + { + { "mode", ["edit"] } + } + } + }; + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().ContainItemsAssignableTo(); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory) + { + // Arrange + GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>()); + SitecoreLayoutRequest request = new() + { + { + "sc_request_headers_key" , new Dictionary() + { + { "mode", ["edit"] }, + { "language", ["en"] }, + { "sc_layoutKind", ["Final"] }, + { "sc_itemid", ["item_1234"] }, + { "sc_version", ["version_1234"] }, + { "sc_site", ["site_1234"] } + } + }, + { + "sc_lang", "en" + } + }; + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + } + } +} From ddb071637e56822de8922b24d4daee92d82c8e1b Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 17 Feb 2025 13:14:54 +1100 Subject: [PATCH 14/38] Added UniTests for EditingChromes being added to response for Pages rendering --- .../Request/Handlers/GraphQL/Constants.cs | 304 ++++++++++++++++++ .../GraphQLEditingServiceHandlerFixture.cs | 221 ++++++++++--- 2 files changed, 482 insertions(+), 43 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs new file mode 100644 index 0000000..e2181c7 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs @@ -0,0 +1,304 @@ +using GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using System.Text.Json; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL +{ + public static class Constants + { + public static GraphQLResponse SimpleEditingLayoutQueryResponse + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement + }, + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + PageInfo = new PageInfo + { + HasNext = false + } + } + } + } + } + }; + } + } + + public static GraphQLResponse EditingLayoutQueryResponseWithDictionaryPaging + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement + }, + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + PageInfo = new PageInfo + { + HasNext = true, + EndCursor = "cursor_value_1234" + } + } + } + } + } + }; + } + } + + public static GraphQLResponse EditingDictionaryResponse + { + get + { + return new GraphQLResponse + { + Data = new EditingDictionaryResponse + { + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + Results = new List + { + new SiteInfoDictionaryItem + { + Key = "key1", + Value = "value1" + }, + new SiteInfoDictionaryItem + { + Key = "key2", + Value = "value2" + } + }, + PageInfo = new PageInfo + { + HasNext = false + } + } + } + } + } + }; + } + } + + public static GraphQLResponse MockEditingLayoutQueryResponse + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse(@"{ ""sitecore"" : {}}").RootElement + }, + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + PageInfo = new PageInfo + { + HasNext = false + } + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_Placeholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", new Placeholder() + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_NestedPlaceholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", new Placeholder + { + new Component + { + Name = "component_1", + Id = "component_1", + Placeholders = new Dictionary + { + { + "nested_placeholder_1", new Placeholder() + } + } + } + } + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_WithComponentInPlaceholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", new Placeholder + { + new Component + { + Name = "component_1", + Id = "component_1" + } + } + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_ComponentInNestedPlaceholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", new Placeholder + { + new Component + { + Name = "component_1", + Id = "component_1", + Placeholders = new Dictionary + { + { + "nested_placeholder_1", new Placeholder + { + new Component + { + Name = "nested_component_2", + Id = "nested_component_2" + } + } + } + } + } + } + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_ComponentWithField + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", new Placeholder + { + new Component + { + Name = "component_1", + Id = "component_1", + Fields = new Dictionary + { + { "field_1", new JsonSerializedField(JsonDocument.Parse("{\"metadata\":{\"datasource\":{\"id\":\"datasource_id\",\"language\":\"en\",\"revision\":\"revision_1\",\"version\":1},\"title\":\"Text\",\"fieldId\":\"field_id\",\"fieldType\":\"Text\",\"rawValue\":\"field_raw_value\"},\"value\":\"field_value\"}")) } + } + } + } + } + } + } + } + }; + } + } + } +} diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 2e3e931..63905ff 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using AutoFixture; using AutoFixture.Idioms; -using Castle.Core.Logging; using FluentAssertions; using GraphQL; using GraphQL.Client.Abstractions; @@ -12,9 +10,10 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; -using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; using Xunit; @@ -27,33 +26,34 @@ public class GraphQLEditingServiceHandlerFixture public static Action AutoSetup => f => { IGraphQLClient client = Substitute.For(); + f.Inject(client); + IGraphQLClientFactory clientFactory = Substitute.For(); - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse + f.Inject(clientFactory); + + ISitecoreLayoutSerializer mockSerializer = Substitute.For(); + f.Inject(mockSerializer); + + SitecoreLayoutRequest request = new() { - Data = new EditingLayoutQueryResponse { - Item = new ItemModel - { - Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement - }, - Site = new Site + "sc_request_headers_key" , new Dictionary() { - SiteInfo = new SiteInfo - { - Dictionary = new SiteInfoDictionary - { - PageInfo = new PageInfo - { - HasNext = false - } - } - } + { "mode", ["edit"] }, + { "language", ["en"] }, + { "sc_layoutKind", ["Final"] }, + { "sc_itemid", ["item_1234"] }, + { "sc_version", ["version_1234"] } } + }, + { + "sc_lang", "en" + }, + { + "sc_site", "site_1234" } - }); - - f.Inject(clientFactory); + }; + f.Inject(request); }; [Theory] @@ -134,33 +134,168 @@ public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory) + public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) { // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.SimpleEditingLayoutQueryResponse); GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>()); - SitecoreLayoutRequest request = new() - { - { - "sc_request_headers_key" , new Dictionary() - { - { "mode", ["edit"] }, - { "language", ["en"] }, - { "sc_layoutKind", ["Final"] }, - { "sc_itemid", ["item_1234"] }, - { "sc_version", ["version_1234"] }, - { "sc_site", ["site_1234"] } - } - }, - { - "sc_lang", "en" - } - }; // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); // Assert result.Errors.Should().BeEmpty(); + await client.Received(1).SendQueryAsync(Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_DictionaryRequestMade(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingLayoutQueryResponseWithDictionaryPaging); + client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingDictionaryResponse); + GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Asset + result.Errors.Should().BeEmpty(); + await client.Received(1).SendQueryAsync(Arg.Any()); + await client.Received(1).SendQueryAsync(Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_Placeholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(2); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["id"].Should().Be($"placeholder_1_{Guid.Empty.ToString()}"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_NestedPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["id"].Should().Be("container-{*}_component_1"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_WithComponentInPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["id"].Should().Be($"component_1"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentInNestedPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["id"].Should().Be($"nested_component_2"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_FieldRenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentWithField); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields.Values.Count.Should().Be(1); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"].Should().BeOfType(); + JsonSerializedField? jsonSerialisedField = result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"] as JsonSerializedField; + jsonSerialisedField.Should().NotBeNull(); + EditableField? editableField = jsonSerialisedField?.Read>(); + editableField.Should().NotBeNull(); + editableField?.OpeningChrome.Should().NotBeNull(); + editableField?.OpeningChrome?.Attributes["chrometype"].Should().Be("field"); + editableField?.OpeningChrome?.Attributes["kind"].Should().Be("open"); + editableField?.OpeningChrome?.Content.Should().Be(@"{""datasource"":{""id"":""datasource_id"",""language"":""en"",""revision"":""revision_1"",""version"":1},""title"":""Text"",""fieldId"":""field_id"",""fieldType"":""Text"",""rawValue"":""field_raw_value""}"); + editableField?.ClosingChrome.Should().NotBeNull(); + editableField?.ClosingChrome?.Attributes["chrometype"].Should().Be("field"); + editableField?.ClosingChrome?.Attributes["kind"].Should().Be("close"); } } -} +} \ No newline at end of file From 328561f4cf2dceae0066666965f6cbf5947c7129 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 17 Feb 2025 13:33:38 +1100 Subject: [PATCH 15/38] Restructured test for Pages config middleware --- .../Middleware/PageSetupMiddlewareFixture.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs index dd5466f..9728b7d 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Text.Json; using AutoFixture; using AutoFixture.Idioms; using FluentAssertions; @@ -148,9 +149,11 @@ public async Task Invoke_ConfigRequest_ValidResponseReturned(RequestDelegate req byte[] contents = memoryStream.ToArray(); string returnedBody = Encoding.UTF8.GetString(contents); - string expectedResponse = "\r\n {\r\n \"components\": [\r\n \"TestComponent\"\r\n ],\r\n \"packages\": {\r\n },\r\n \"editMode\": \"metadata\"\r\n }\r\n "; - Assert.Equal(expectedResponse, returnedBody); - + var jsonDoc = JsonDocument.Parse(returnedBody); + jsonDoc.RootElement.TryGetProperty("editMode", out JsonElement editNode).Should().BeTrue(); + editNode.GetString().Should().Be("metadata"); + jsonDoc.RootElement.TryGetProperty("components", out JsonElement componentsNode).Should().BeTrue(); + componentsNode[0].GetString().Should().Be("TestComponent"); httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); httpContext.Response.Headers.AccessControlAllowOrigin.Should().Equal(ValidEditingOrigin); httpContext.Response.Headers.AccessControlAllowMethods.Should().Equal("GET, POST, OPTIONS, PUT, PATCH, DELETE"); From c2851cbf0e60faa3bc5fe130701fd171550e587a Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 4 Mar 2025 17:54:48 +1100 Subject: [PATCH 16/38] Fixed bug with ImageTagHelper and ViewCompponent --- .../Extensions/RenderingEngineOptionsExtensions.cs | 2 +- .../Rendering/ViewComponentComponentRenderer.cs | 6 ++++-- .../TagHelpers/Fields/ImageTagHelper.cs | 2 +- .../TagHelpers/Fields/TextFieldTagHelper.cs | 3 ++- .../Rendering/ViewComponentComponentRendererFixture.cs | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs index bbe9951..aea0ff5 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs @@ -151,7 +151,7 @@ public static RenderingEngineOptions AddViewComponent( ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); - ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName); + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName, viewComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs index 8c2e97f..de48e2a 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs @@ -29,16 +29,18 @@ public ViewComponentComponentRenderer(string locator) /// /// A predicate to use when attempting to match a layout component. /// The string to use when locating the View Component. + /// The string to use describe the name of the components. /// An instance of . - public static ComponentRendererDescriptor Describe(Predicate match, string locator) + public static ComponentRendererDescriptor Describe(Predicate match, string locator, string componentName) { ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(locator); + ArgumentException.ThrowIfNullOrWhiteSpace(componentName); return new ComponentRendererDescriptor( match, sp => ActivatorUtilities.CreateInstance(sp, locator), - string.Empty); + componentName); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs index 62446e9..7cec677 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs @@ -66,7 +66,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) ImageField? field = ImageModel ?? For?.Model as ImageField; - if (field == null || string.IsNullOrWhiteSpace(field.Value.Src)) + if (field == null || (string.IsNullOrWhiteSpace(field.Value.Src) && (field.OpeningChrome == null && field.ClosingChrome == null))) { return; } diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs index 6c9938e..1f04df7 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs @@ -52,6 +52,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { html += chromeRenderer.Render(field.OpeningChrome); html += "
"; + isHtml = true; } if (outputEditableMarkup || (ConvertNewLines && NewLineRegex().IsMatch(value))) @@ -66,7 +67,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (Editable && field.ClosingChrome != null) { - html += "
"; + html += "
"; html += chromeRenderer.Render(field.ClosingChrome); } diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs index 083f0b5..4eb5761 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Rendering/ViewComponentComponentRendererFixture.cs @@ -48,7 +48,7 @@ public void Describe_LocatorIsNotNullOrEmpty_DescriptorCanCreateComponentRendere ServiceContainer services = new(); // Act - ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(_ => true, Locator); + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(_ => true, Locator, Locator); // Assert descriptor.Should().NotBeNull(); From 4d1c1dd3f427d7ccb14dd03b31db86eb06e42413 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 14 Mar 2025 13:24:52 +1100 Subject: [PATCH 17/38] Moved PagesSetupMiddleware in a Controller, other small fixes --- .../Response/Model/IEditableField.cs | 1 - .../Response/Model/MetaData.cs | 1 - .../Configuration/PagesOptions.cs | 5 + .../Controllers/PagesSetupController.cs | 149 +++++++++++++++ .../PagesAppConfigurationExtensions.cs | 25 ++- .../Middleware/PageSetupMiddleware.cs | 180 ------------------ .../Middleware/PagesRenderMiddleware.cs | 6 +- .../Models/PagesConfigResponse.cs | 22 +++ .../Response/ClientData.cs | 2 +- .../PagesSetupControllerFixture.cs} | 123 ++++++------ .../PagesAppConfigurationExtensionsFixture.cs | 3 +- 11 files changed, 260 insertions(+), 257 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs delete mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs rename tests/Sitecore.AspNetCore.SDK.Pages.Tests/{Middleware/PageSetupMiddlewareFixture.cs => Controllers/PagesSetupControllerFixture.cs} (66%) diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs index 590f044..14e158d 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/IEditableField.cs @@ -18,6 +18,5 @@ public interface IEditableField : IField /// /// Gets or sets the EditableChrome used to render the closing chrome for this field. /// - public EditableChrome? ClosingChrome { get; set; } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs index ac21885..fd83109 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs @@ -29,5 +29,4 @@ public class MetaData /// Gets or sets the DataSource. /// public DataSource? DataSource { get; set; } - } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs index 0049390..5ceab69 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -5,6 +5,11 @@ /// public class PagesOptions { + /// + /// Key used to define the settings section in config. + /// + public static readonly string Key = "SitecoreXmcPages"; + /// /// Gets or sets the config endpoint for Pages MetaData mode. /// diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs new file mode 100644 index 0000000..f1a4344 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.Pages.Models; +using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; + +namespace Sitecore.AspNetCore.SDK.Pages.Controllers +{ + /// + /// Pages Setup Controller, used to handle the configuration requests that MetaData editing mode uses to validate the editing host can connect successfully. + /// + /// The Sitecore Pages configuration options. + /// The to use for logging. + /// The RenderingEngineOptions, used to retriece a list of all registered renderings for the applications. + public class PagesSetupController(IOptions options, ILogger logger, IOptions renderingEngineOptions) : ControllerBase + { + private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly RenderingEngineOptions renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); + + /// + /// The Config endpoint used to inform Pages how this editing host is configured and which components are implemented. + /// + /// PagesConfigResponse object. + [HttpGet] + public ActionResult Config() + { + if (IsValidPagesConfigRequest(Request)) + { + logger.LogDebug("Processing valid Pages Config request"); + SetConfigResponseHeaders(Response); + return Ok(BuildConfigResponseBody()); + } + + return BadRequest(); + } + + /// + /// The render endpoint used to redirect the Pages application to the correct page. + /// + /// Redirect response. + [HttpGet] + public IActionResult Render() + { + if (IsValidPagesRenderRequest(Request)) + { + logger.LogDebug("Processing valid Pages Render request"); + PagesRenderArgs args = ParseQueryStringArgs(Request); + return Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}&sc_lang={args.Language}&sc_site={args.Site}&sc_layoutKind={args.LayoutKind}&secret={args.EditingSecret}&tenant_id={args.TenantId}&route={args.Route}"); + } + + return BadRequest(); + } + + private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) + { + return new PagesRenderArgs + { + ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, + EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, + Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, + LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, + Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, + Route = request.Query["route"].FirstOrDefault() ?? string.Empty, + Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, + Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0, + TenantId = request.Query["tenant_id"].FirstOrDefault() ?? string.Empty, + }; + } + + private bool IsValidPagesRenderRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + + if (!IsValidEditingSecret(httpRequest)) + { + logger.LogError("Invalid Pages Editing Secret Value"); + return false; + } + + return true; + } + + private PagesConfigResponse BuildConfigResponseBody() + { + return new PagesConfigResponse + { + EditMode = "metadata", + Components = renderingEngineOptions.RendererRegistry.Select(x => x.Value.ComponentName).ToList() + }; + } + + private void SetConfigResponseHeaders(HttpResponse httpResponse) + { + httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; + httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; + httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; + httpResponse.StatusCode = StatusCodes.Status200OK; + httpResponse.ContentType = "application/json"; + } + + private bool IsValidPagesConfigRequest(HttpRequest httpRequest) + { + ArgumentNullException.ThrowIfNull(httpRequest); + + if (!IsValidEditingSecret(httpRequest)) + { + logger.LogError("Invalid Pages Editing Secret Value"); + return false; + } + + if (!RequestHasValidEditingOrigin(httpRequest)) + { + logger.LogError("Invalid Pages Editing Origin"); + return false; + } + + return true; + } + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == options.EditingSecret) + { + return true; + } + } + + return false; + } + + private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) + { + if (httpRequest.Headers.Origin == options.ValidEditingOrigin) + { + return true; + } + + return false; + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index 5fd07bb..f19395b 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -25,16 +25,32 @@ public static class PagesAppConfigurationExtensions /// Registers the Sitecore Experience Editor middleware into the . /// /// The instance of the to extend. + /// The Pages options used to configure Pages MetaData edting. /// The so that additional calls can be chained. - public static IApplicationBuilder UseSitecorePages(this IApplicationBuilder app) + public static IApplicationBuilder UseSitecorePages(this WebApplication app, PagesOptions options) { ArgumentNullException.ThrowIfNull(app); - object? experienceEditorMarker = app.ApplicationServices.GetService(typeof(PagesMarkerService)); - if (experienceEditorMarker != null) + object? pagesMarker = app.Services.GetService(typeof(PagesMarkerService)); + if (pagesMarker != null) { - app.UseMiddleware(); app.UseMiddleware(); + + if (!string.IsNullOrEmpty(options.ConfigEndpoint)) + { + app.MapControllerRoute( + "pages-config", + options.ConfigEndpoint, + new { controller = "PagesSetup", action = "Config" }); + } + + if (!string.IsNullOrEmpty(options.RenderEndpoint)) + { + app.MapControllerRoute( + "pages-render", + options.RenderEndpoint, + new { controller = "PagesSetup", action = "Render" }); + } } return app; @@ -89,7 +105,6 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe /// /// The to configure. /// The so that additional calls can be chained. - public static ISitecoreLayoutClientBuilder AddSitecorePagesHandler( this ISitecoreLayoutClientBuilder builder) { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs deleted file mode 100644 index c3ffa37..0000000 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PageSetupMiddleware.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Sitecore.AspNetCore.SDK.Pages.Configuration; -using Sitecore.AspNetCore.SDK.Pages.Models; -using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; -using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; -using System.Net.Http; -using System.Text.Json; - -namespace Sitecore.AspNetCore.SDK.Pages.Middleware; - -/// -/// The Pages middleware implementation that handles GET requests from the Sitecore Pages in MetaData Editing mode -/// and wraps the response HTML in a JSON format. -/// -/// -/// Initializes a new instance of the class. -/// -/// The next middleware to call. -/// The Sitecore Pages configuration options. -/// The to use for logging. -/// The RenderingEngineOptions, used to retriece a list of all registered renderings for the applications -public class PageSetupMiddleware(RequestDelegate next, IOptions options, ILogger logger, IOptions renderingEngineOptions) -{ - private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); - private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); - private readonly RenderingEngineOptions renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); - - /// - /// The middleware Invoke method. - /// - /// The current . - /// A Task to support async calls. - public async Task Invoke(HttpContext httpContext) - { - if (IsValidPagesConfigRequest(httpContext.Request)) - { - logger.LogDebug("Processing valid Pages Config request"); - await BuildResponse(httpContext.Response); - return; - } - - if (IsValidPagesRenderRequest(httpContext.Request)) - { - logger.LogDebug("Processing valid Pages Render request"); - - PerformPagesRedirect(httpContext, ParseQueryStringArgs(httpContext.Request)); - - return; - } - - await next(httpContext).ConfigureAwait(false); - } - - private void PerformPagesRedirect(HttpContext httpContext, PagesRenderArgs args) - { - httpContext.Response.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; - httpContext.Response.Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}&sc_lang={args.Language}&sc_site={args.Site}&sc_layoutKind={args.LayoutKind}&secret={args.EditingSecret}&tenant_id={args.TenantId}&route={args.Route}", permanent: false); - } - - private bool IsValidPagesRenderRequest(HttpRequest httpRequest) - { - ArgumentNullException.ThrowIfNull(httpRequest); - if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.RenderEndpoint, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!IsValidEditingSecret(httpRequest)) - { - logger.LogError("Invalid Pages Editing Secret Value"); - return false; - } - - return true; - } - - private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) - { - return new PagesRenderArgs - { - ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, - EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, - Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, - LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, - Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, - Route = request.Query["route"].FirstOrDefault() ?? string.Empty, - Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, - Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0, - TenantId = request.Query["tenant_id"].FirstOrDefault() ?? string.Empty, - }; - } - - private async Task BuildResponse(HttpResponse httpResponse) - { - httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; - httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; - httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; - httpResponse.StatusCode = StatusCodes.Status200OK; - httpResponse.ContentType = "application/json"; - - Stream realResponseStream = httpResponse.Body; - try - { - MemoryStream tmpResponseBuffer = new(); - - httpResponse.Body = tmpResponseBuffer; - - tmpResponseBuffer.Position = 0; - string componentNames = string.Join(",\r\n", renderingEngineOptions.RendererRegistry.Select(x => $"\"{x.Value.ComponentName}\"")); - string responseBody = $@" - {{ - ""components"": [ - {componentNames} - ], - ""packages"": {{ - }}, - ""editMode"": ""metadata"" - }} - "; - - await using StreamWriter realResponseWriter = new(realResponseStream); - await realResponseWriter.WriteAsync(responseBody).ConfigureAwait(false); - } - finally - { - httpResponse.Body = realResponseStream; - } - } - - private bool IsValidEditingSecret(HttpRequest httpRequest) - { - if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) - { - string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; - if (editingSecret == options.EditingSecret) - { - return true; - } - } - - return false; - } - - private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) - { - if (httpRequest.Headers.Origin == options.ValidEditingOrigin) - { - return true; - } - - return false; - } - - private bool IsValidPagesConfigRequest(HttpRequest httpRequest) - { - ArgumentNullException.ThrowIfNull(httpRequest); - if (httpRequest.Method != HttpMethods.Get || !httpRequest.Path.Value!.Equals(options.ConfigEndpoint, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!IsValidEditingSecret(httpRequest)) - { - logger.LogError("Invalid Pages Editing Secret Value"); - return false; - } - - if (!RequestHasValidEditingOrigin(httpRequest)) - { - logger.LogError("Invalid Pages Editing Origin"); - return false; - } - - return true; - } -} \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 7df516e..7940b7c 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -18,20 +18,20 @@ namespace Sitecore.AspNetCore.SDK.Pages.Middleware; /// and wraps the response HTML in a JSON format. /// /// -/// Initializes a new instance of the class. +/// Initializes a new instance of the class. /// /// The next middleware to call. /// The Sitecore Pages configuration options. /// The to map the HttpRequest to a Layout Service request. /// The layout service client. /// The to use for logging. -public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService, ILogger logger) +public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService, ILogger logger) { private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); private readonly ISitecoreLayoutRequestMapper _requestMapper = requestMapper ?? throw new ArgumentNullException(nameof(requestMapper)); private readonly ISitecoreLayoutClient layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); /// /// The middleware Invoke method. diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs new file mode 100644 index 0000000..b192052 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs @@ -0,0 +1,22 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Models; + +/// +/// The response object for the Pages Config endpoint. +/// +public class PagesConfigResponse +{ + /// + /// Gets or sets the edit mode for the Pages Config endpoint. + /// + public List Components { get; set; } = new(); + + /// + /// Gets or sets the edit mode for the Pages Config endpoint. + /// + public string? EditMode { get; set; } + + /// + /// Gets or sets the edit mode for the Pages Config endpoint. + /// + public object Packages { get; set; } = new(); +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs index 2fe1b0e..c257897 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/ClientData.cs @@ -8,7 +8,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.Response; public class ClientData { /// - /// Gets or sets the Canvas State data of the Editing Request + /// Gets or sets the Canvas State data of the Editing Request. /// [JsonPropertyName("hrz-canvas-state")] public CanvasState? CanvasState { get; set; } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs similarity index 66% rename from tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs rename to tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs index 9728b7d..c986608 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Middleware/PageSetupMiddlewareFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -1,10 +1,9 @@ using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Json; using AutoFixture; using AutoFixture.Idioms; using FluentAssertions; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -12,14 +11,15 @@ using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; using Sitecore.AspNetCore.SDK.Pages.Configuration; -using Sitecore.AspNetCore.SDK.Pages.Middleware; +using Sitecore.AspNetCore.SDK.Pages.Controllers; +using Sitecore.AspNetCore.SDK.Pages.Models; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; using Xunit; -namespace Sitecore.AspNetCore.SDK.Pages.Tests.Middleware +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Controllers { - public class PageSetupMiddlewareFixture + public class PagesSetupControllerFixture { private const string ValidConfigEndpoint = "/api/editing/config"; private const string ValidRenderEndpoint = "/api/editing/render"; @@ -45,7 +45,7 @@ public class PageSetupMiddlewareFixture pagesOptions.Value.Returns(PagesOptionsValues); f.Inject(pagesOptions); - ILogger logger = Substitute.For>(); + ILogger logger = Substitute.For>(); f.Inject(logger); IOptions renderingEngineOptions = Substitute.For>(); @@ -67,135 +67,126 @@ public class PageSetupMiddlewareFixture [AutoNSubstituteData] public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) { - guard.VerifyConstructors(); + guard.VerifyConstructors(); } [Theory] [AutoNSubstituteData] - public async Task Invoke_RequestIsntConfigOrRender_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + public void ConfigRoute_InvalidEditingSecret_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) { // Arrange - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); - HttpContext httpContext = Substitute.For(); - HttpRequest httpRequest = Substitute.For(); - httpRequest.Method.Returns("Post"); - httpContext.Request.Returns(httpRequest); - - // Act - await sut.Invoke(httpContext); - - // Assert - await requestDelegate.Received()(httpContext); - } - - [Theory] - [AutoNSubstituteData] - public async Task Invoke_ConfigRequest_InvalidEditingSecret_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) - { - // Arrange - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); HttpContext httpContext = Substitute.For(); httpContext.Request.Method.Returns("GET"); httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues("incorrect_secret_value") } })); + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; // Act - await sut.Invoke(httpContext); + ActionResult response = sut.Config(); // Assert logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Secret Value"), null, Arg.Any>()); - await requestDelegate.Received()(httpContext); + response.Result.Should().BeOfType(); } [Theory] [AutoNSubstituteData] - public async Task Invoke_ConfigRequest_InvalidEditingOrigin_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + public void ConfigRoute_InvalidEditingOrigin_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) { // Arrange - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); HttpContext httpContext = Substitute.For(); httpContext.Request.Method.Returns("GET"); httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues(ValidEditingSecret) } })); httpContext.Request.Headers.Returns(new HeaderDictionary(new Dictionary { { "Origin", new StringValues("http://an.invalid.origin.domain") } })); + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; // Act - await sut.Invoke(httpContext); + ActionResult response = sut.Config(); // Assert logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Origin"), null, Arg.Any>()); - await requestDelegate.Received()(httpContext); + response.Result.Should().BeOfType(); } [Theory] [AutoNSubstituteData] - public async Task Invoke_ConfigRequest_ValidResponseReturned(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + public void ConfigRoute_ValidRequest_OkResponseReturned(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) { // Arrange - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); HttpContext httpContext = Substitute.For(); httpContext.Request.Method.Returns("GET"); httpContext.Request.Path.Returns(new PathString(ValidConfigEndpoint)); httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues(ValidEditingSecret) } })); httpContext.Request.Headers.Returns(new HeaderDictionary(new Dictionary { { "Origin", new StringValues(ValidEditingOrigin) } })); - MemoryStream memoryStream = new(); - httpContext.Response.Body = memoryStream; + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; // Act - await sut.Invoke(httpContext); + ActionResult response = sut.Config(); // Assert logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Config request"), null, Arg.Any>()); - - byte[] contents = memoryStream.ToArray(); - string returnedBody = Encoding.UTF8.GetString(contents); - var jsonDoc = JsonDocument.Parse(returnedBody); - jsonDoc.RootElement.TryGetProperty("editMode", out JsonElement editNode).Should().BeTrue(); - editNode.GetString().Should().Be("metadata"); - jsonDoc.RootElement.TryGetProperty("components", out JsonElement componentsNode).Should().BeTrue(); - componentsNode[0].GetString().Should().Be("TestComponent"); httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); httpContext.Response.Headers.AccessControlAllowOrigin.Should().Equal(ValidEditingOrigin); httpContext.Response.Headers.AccessControlAllowMethods.Should().Equal("GET, POST, OPTIONS, PUT, PATCH, DELETE"); httpContext.Response.StatusCode.Should().Be(StatusCodes.Status200OK); httpContext.Response.ContentType.Should().Be("application/json"); - - await requestDelegate.DidNotReceive()(httpContext); + response.Result.Should().BeOfType(); + response.Result.As().Value.Should().NotBeNull(); + response.Result.As().Value.As().Should().NotBeNull(); + response.Result.As().Value.As().EditMode.Should().Be("metadata"); + response.Result.As().Value.As().Components.Count.Should().Be(1); + response.Result.As().Value.As().Components[0].Should().Be("TestComponent"); } [Theory] [AutoNSubstituteData] - public async Task Invoke_RenderRequest_InvalidEditingSecret_NextDelegateCalled(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + public void RenderRoute_InvalidEditingSecret_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) { // Arrange - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); HttpContext httpContext = Substitute.For(); httpContext.Request.Method.Returns("GET"); httpContext.Request.Path.Returns(new PathString(ValidRenderEndpoint)); httpContext.Request.Query.Returns(new QueryCollection(new Dictionary { { "secret", new StringValues("incorrect_secret_value") } })); + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; // Act - await sut.Invoke(httpContext); + IActionResult response = sut.Render(); // Assert logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Secret Value"), null, Arg.Any>()); - await requestDelegate.Received()(httpContext); + response.Should().BeOfType(); } [Theory] [AutoNSubstituteData] - public async Task Invoke_RenderRequest_ValidResponseReturned(RequestDelegate requestDelegate, IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + public void RenderRoute_ValidRequest_OkResponseReturned(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) { // Arrange string expectedRoute = "test_route"; string expectedMode = "test_mode"; - string expectedItemId = "test_item_id"; - string expectedVersion = "test_version"; + Guid expectedItemId = Guid.NewGuid(); + int expectedVersion = 0; string expectedLanguage = "test_lang"; string expectedSite = "test_site"; string expectedLayoutKind = "test_layoutKind"; string expectedTenantId = "test_tenant_id"; - PageSetupMiddleware sut = new(requestDelegate, pageOptions, logger, renderingEngineOptions); HttpContext httpContext = Substitute.For(); httpContext.Request.Method.Returns("GET"); httpContext.Request.Path.Returns(new PathString(ValidRenderEndpoint)); @@ -204,32 +195,34 @@ public async Task Invoke_RenderRequest_ValidResponseReturned(RequestDelegate req new Dictionary { { "secret", new StringValues(ValidEditingSecret) }, - { "sc_itemid", new StringValues(expectedItemId) }, + { "sc_itemid", new StringValues(expectedItemId.ToString()) }, { "sc_lang", new StringValues(expectedLanguage) }, { "sc_layoutKind", new StringValues(expectedLayoutKind) }, { "mode", new StringValues(expectedMode) }, { "route", new StringValues(expectedRoute) }, { "sc_site", new StringValues(expectedSite) }, - { "sc_version", new StringValues(expectedVersion) }, + { "sc_version", new StringValues(expectedVersion.ToString()) }, { "tenant_id", new StringValues(expectedTenantId) } })); - HttpResponse httpResponse = Substitute.For(); httpContext.Response.Returns(httpResponse); MemoryStream memoryStream = new(); httpResponse.Body = memoryStream; + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; // Act - await sut.Invoke(httpContext); + IActionResult response = sut.Render(); // Assert logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Render request"), null, Arg.Any>()); - - httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); + response.Should().BeOfType(); + response.As().Permanent.Should().BeFalse(); string validRedirectString = $"{expectedRoute}?mode={expectedMode}&sc_itemid={expectedItemId}&sc_version={expectedVersion}&sc_lang={expectedLanguage}&sc_site={expectedSite}&sc_layoutKind={expectedLayoutKind}&secret={ValidEditingSecret}&tenant_id={expectedTenantId}&route={expectedRoute}"; - httpResponse.ReceivedWithAnyArgs().Redirect(validRedirectString, permanent: false); - - await requestDelegate.DidNotReceive()(httpContext); + response.As().Url.Should().Be(validRedirectString); } } } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs index e7aa3ea..06e66cc 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Extensions; using Xunit; @@ -10,7 +11,7 @@ public class PagesAppConfigurationExtensionsFixture public void UseSitecorePages_AppIsNull_ExceptionIsThrown() { // Act - Action action = () => PagesAppConfigurationExtensions.UseSitecorePages(null!); + Action action = () => PagesAppConfigurationExtensions.UseSitecorePages(null!, new PagesOptions()); // Assert action.Should().Throw(); From 9e5608ef42857b8f12bb3058bc66b4a5e0e93bcb Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 14 Mar 2025 13:44:14 +1100 Subject: [PATCH 18/38] Moved log messages into Resources RESX --- .../Controllers/PagesSetupController.cs | 11 +- .../Middleware/PagesRenderMiddleware.cs | 3 +- .../Properties/Resources.Designer.cs | 153 ++++++++++++++++++ .../Properties/Resources.resx | 150 +++++++++++++++++ .../GraphQL/GraphQLEditingServiceHandler.cs | 7 +- .../TagHelpers/EditingScriptsTagHelper.cs | 5 +- .../PagesSetupControllerFixture.cs | 2 +- 7 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index f1a4344..345ba0a 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -6,6 +6,7 @@ using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Middleware; using Sitecore.AspNetCore.SDK.Pages.Models; +using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; namespace Sitecore.AspNetCore.SDK.Pages.Controllers @@ -31,7 +32,7 @@ public ActionResult Config() { if (IsValidPagesConfigRequest(Request)) { - logger.LogDebug("Processing valid Pages Config request"); + logger.LogDebug(Resources.Debug_ProcessingValidPagesConfigRequest); SetConfigResponseHeaders(Response); return Ok(BuildConfigResponseBody()); } @@ -48,7 +49,7 @@ public IActionResult Render() { if (IsValidPagesRenderRequest(Request)) { - logger.LogDebug("Processing valid Pages Render request"); + logger.LogDebug(Resources.Debug_ProcessingValidPagesRenderRequest); PagesRenderArgs args = ParseQueryStringArgs(Request); return Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}&sc_lang={args.Language}&sc_site={args.Site}&sc_layoutKind={args.LayoutKind}&secret={args.EditingSecret}&tenant_id={args.TenantId}&route={args.Route}"); } @@ -78,7 +79,7 @@ private bool IsValidPagesRenderRequest(HttpRequest httpRequest) if (!IsValidEditingSecret(httpRequest)) { - logger.LogError("Invalid Pages Editing Secret Value"); + logger.LogError(Resources.Error_InvalidPagesEditingSecretValue); return false; } @@ -109,13 +110,13 @@ private bool IsValidPagesConfigRequest(HttpRequest httpRequest) if (!IsValidEditingSecret(httpRequest)) { - logger.LogError("Invalid Pages Editing Secret Value"); + logger.LogError(Resources.Error_InvalidPagesEditingSecretValue); return false; } if (!RequestHasValidEditingOrigin(httpRequest)) { - logger.LogError("Invalid Pages Editing Origin"); + logger.LogError(Resources.Error_InvalidPagesEditingOrigin); return false; } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 7940b7c..97eda8a 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -7,6 +7,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; @@ -51,7 +52,7 @@ public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewCompo // this protects from multiple time executions when Global and Attribute based configurations are used at the same time. if (httpContext.Items.ContainsKey(nameof(PagesRenderMiddleware))) { - throw new ApplicationException("PagesRenderMiddleware already registered. Have you "); + throw new ApplicationException(Resources.Exception_PagesRenderMiddlewareAlreadyRegistered); } if (httpContext.GetSitecoreRenderingContext() == null) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs new file mode 100644 index 0000000..66727cc --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Sitecore.AspNetCore.SDK.Pages.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Sitecore.AspNetCore.SDK.Pages.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Layout Service GraphQL Response : {responseDataLayout}. + /// + internal static string Debug_LayoutServiceGraphQLResponse { + get { + return ResourceManager.GetString("Debug_LayoutServiceGraphQLResponse", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Layout Service Response JSON. + /// + internal static string Debug_LayoutServiceResponseJSON { + get { + return ResourceManager.GetString("Debug_LayoutServiceResponseJSON", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processing valid Pages Config request. + /// + internal static string Debug_ProcessingValidPagesConfigRequest { + get { + return ResourceManager.GetString("Debug_ProcessingValidPagesConfigRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Processing valid Pages Render request.. + /// + internal static string Debug_ProcessingValidPagesRenderRequest { + get { + return ResourceManager.GetString("Debug_ProcessingValidPagesRenderRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Pages Editing Origin. + /// + internal static string Error_InvalidPagesEditingOrigin { + get { + return ResourceManager.GetString("Error_InvalidPagesEditingOrigin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Pages Editing Secret Value. + /// + internal static string Error_InvalidPagesEditingSecretValue { + get { + return ResourceManager.GetString("Error_InvalidPagesEditingSecretValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EditingScriptsTagHelper: Sitecore RenderingContext is Null. + /// + internal static string Exception_EditingScriptsTagHelperSitecoreRenderingContextNull { + get { + return ResourceManager.GetString("Exception_EditingScriptsTagHelperSitecoreRenderingContextNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to EditingScriptsTagHelper: Unable to process ContextRawData. + /// + internal static string Exception_EditingScriptsTagHelperUnableToProcessContextRawData { + get { + return ResourceManager.GetString("Exception_EditingScriptsTagHelperUnableToProcessContextRawData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GraphQLEditingServiceHandler: Error attempting to process non-editing request. + /// + internal static string Exception_ErrorAttemptingToProcessNonEditingRequest { + get { + return ResourceManager.GetString("Exception_ErrorAttemptingToProcessNonEditingRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to PagesRenderMiddleware already registered. + /// + internal static string Exception_PagesRenderMiddlewareAlreadyRegistered { + get { + return ResourceManager.GetString("Exception_PagesRenderMiddlewareAlreadyRegistered", resourceCulture); + } + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx new file mode 100644 index 0000000..38d69a8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Layout Service GraphQL Response : {responseDataLayout} + + + Layout Service Response JSON + + + Processing valid Pages Config request + + + Processing valid Pages Render request. + + + Invalid Pages Editing Origin + + + Invalid Pages Editing Secret Value + + + EditingScriptsTagHelper: Unable to process ContextRawData + + + GraphQLEditingServiceHandler: Error attempting to process non-editing request + + + PagesRenderMiddleware already registered + + + EditingScriptsTagHelper: Sitecore RenderingContext is Null + + \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index e29139c..fc50da3 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -11,6 +11,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Properties; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -38,7 +39,7 @@ public async Task Request(SitecoreLayoutRequest request, if (!IsEditingRequest(request)) { - throw new ArgumentException("GraphQLEditingServiceHandler: Error attempting to process non-editing request"); + throw new ArgumentException(Resources.Exception_ErrorAttemptingToProcessNonEditingRequest); } List errors = []; @@ -336,7 +337,7 @@ query EditingQuery( if (logger.IsEnabled(LogLevel.Debug)) { - logger.LogDebug("Layout Service GraphQL Response : {responseDataLayout}", response.Data.Item); + logger.LogDebug(Resources.Debug_LayoutServiceGraphQLResponse, response.Data.Item); } SitecoreLayoutResponseContent? content = null; @@ -354,7 +355,7 @@ query EditingQuery( if (logger.IsEnabled(LogLevel.Debug)) { object? formattedDeserializeObject = JsonSerializer.Deserialize(json); - logger.LogDebug("Layout Service Response JSON : {formattedDeserializeObject}", formattedDeserializeObject); + logger.LogDebug(Resources.Debug_LayoutServiceResponseJSON, formattedDeserializeObject); } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs index df5cfdf..7dff1b0 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.Pages.Response; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; @@ -25,7 +26,7 @@ public class EditingScriptsTagHelper : TagHelper public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext() ?? - throw new NullReferenceException("EditingScriptsTagHelper: Sitecore RenderingContext is Null"); + throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperSitecoreRenderingContextNull); output.TagName = string.Empty; string html = string.Empty; @@ -35,7 +36,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); if (editingContext == null) { - throw new NullReferenceException("EditingScriptsTagHelper: Unable to process ContextRawData"); + throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperUnableToProcessContextRawData); } foreach (string script in editingContext.ClientScripts ?? []) diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs index c986608..039f5bb 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -218,7 +218,7 @@ public void RenderRoute_ValidRequest_OkResponseReturned(IOptions p IActionResult response = sut.Render(); // Assert - logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Render request"), null, Arg.Any>()); + logger.Received().Log(LogLevel.Debug, Arg.Any(), Arg.Is(o => o.ToString() == "Processing valid Pages Render request."), null, Arg.Any>()); response.Should().BeOfType(); response.As().Permanent.Should().BeFalse(); string validRedirectString = $"{expectedRoute}?mode={expectedMode}&sc_itemid={expectedItemId}&sc_version={expectedVersion}&sc_lang={expectedLanguage}&sc_site={expectedSite}&sc_layoutKind={expectedLayoutKind}&secret={ValidEditingSecret}&tenant_id={expectedTenantId}&route={expectedRoute}"; From fb2f7725d5795ec9da5e47862dbca6ce849e30d5 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 14 Mar 2025 13:47:10 +1100 Subject: [PATCH 19/38] Added Resources RESX to Pages project --- .../Sitecore.AspNetCore.SDK.Pages.csproj | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj index 683e8a8..5efbcf4 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj +++ b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj @@ -11,4 +11,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + From d0938ba6c4c2d499367298be84d2e99d4700ebe6 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 14 Mar 2025 14:22:22 +1100 Subject: [PATCH 20/38] Refactored PlaceHolder processing to use Stack approach instead of recursion --- .../GraphQL/GraphQLEditingServiceHandler.cs | 82 +++++++++++++------ .../Handlers/GraphQL/PlaceholderWorkItem.cs | 33 ++++++++ 2 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index fc50da3..82dc9b0 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -22,7 +22,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// The to use for logging. /// The GraphQlClientFactory used to generate instances of the GraphQl client. /// The serializer to handle response data. -public class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, +public partial class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, ISitecoreLayoutSerializer serializer, ILogger logger) : ILayoutRequestHandler @@ -93,43 +93,77 @@ private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? conte private static Placeholder ProcessPlaceholder(string name, string id, Placeholder placeholderFeatures) { - Placeholder updatedPlaceholders = []; + Placeholder result = new(); - AddPlaceholderOpeningChrome(name, id, updatedPlaceholders); + // Create a separate class outside of this method for the work item + // to avoid nested class compilation issues + var workStack = new Stack(); + workStack.Push(new PlaceholderWorkItem(name, id, placeholderFeatures, result)); - foreach (var feature in placeholderFeatures) + while (workStack.Count > 0) { - if (feature is Component component) - { - AddRenderingOpeningChrome(updatedPlaceholders, component); + var current = workStack.Pop(); + var output = current.Output; + + // Add opening chrome for placeholder + AddPlaceholderOpeningChrome(current.Name, current.Id, output); - var updatedFields = new Dictionary(); - foreach (var field in component.Fields) + // Process all features in this placeholder + foreach (var feature in current.Features) + { + if (feature is Component component) { - ProcessField(updatedFields, field); - } + // Add opening chrome + AddRenderingOpeningChrome(output, component); - component.Fields = updatedFields; + // Process fields + Dictionary updatedFields = new(); + foreach (var field in component.Fields) + { + ProcessField(updatedFields, field); + } + component.Fields = updatedFields; - updatedPlaceholders.Add(component); + // Add the component to the output + output.Add(component); - foreach (var componentPlaceholder in component.Placeholders) - { + // Process component placeholders before adding closing chrome + if (component.Placeholders.Count > 0) { - string componentPlaceholderName = componentPlaceholder.Key; - Placeholder componentPlaceholderFeatures = componentPlaceholder.Value; - - component.Placeholders[componentPlaceholderName] = ProcessPlaceholder("container-{*}", component.Id, componentPlaceholderFeatures); + // For each placeholder in the component, add it to the work stack + foreach (var placeholder in component.Placeholders.ToList()) + { + string placeholderKey = placeholder.Key; + Placeholder placeholderValue = placeholder.Value; + + // Create a new placeholder to hold the processed content + Placeholder processedPlaceholder = new(); + + // Add a work item to process this placeholder + workStack.Push(new PlaceholderWorkItem( + "container-{*}", + component.Id, + placeholderValue, + processedPlaceholder, + component, + placeholderKey + )); + + // Store the processed placeholder for later assignment + component.Placeholders[placeholderKey] = processedPlaceholder; + } } - } - AddRenderingClosingChrome(updatedPlaceholders); + // Add closing chrome for the component + AddRenderingClosingChrome(output); + } } - } - AddPlaceholderClosingChrome(updatedPlaceholders); + // Add closing chrome for placeholder + AddPlaceholderClosingChrome(output); + } - return updatedPlaceholders; + return result; } private static void AddRenderingClosingChrome(Placeholder updatedPlaceholders) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs new file mode 100644 index 0000000..4693c18 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs @@ -0,0 +1,33 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +public partial class GraphQLEditingServiceHandler +{ + // Define the work item class outside of the method to avoid nested class issues + private class PlaceholderWorkItem + { + public string Name { get; } + public string Id { get; } + public Placeholder Features { get; } + public Placeholder Output { get; } + public Component ParentComponent { get; } + public string PlaceholderKey { get; } + + public PlaceholderWorkItem( + string name, + string id, + Placeholder features, + Placeholder output, + Component parentComponent = null, + string placeholderKey = null) + { + Name = name; + Id = id; + Features = features; + Output = output; + ParentComponent = parentComponent; + PlaceholderKey = placeholderKey; + } + } +} \ No newline at end of file From d380f8ea59b2aa377d1d6e3f36cfd13bdfb3e783 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 17 Mar 2025 10:25:21 +1100 Subject: [PATCH 21/38] Improved logic controlling when Pages middleware executes --- .../Middleware/PagesRenderMiddleware.cs | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 97eda8a..9718c88 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; @@ -47,9 +48,8 @@ public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewCompo ArgumentNullException.ThrowIfNull(viewComponentHelper); ArgumentNullException.ThrowIfNull(htmlHelper); - if (IsEditingRequest(httpContext)) + if (IsValidEditingRequest(httpContext)) { - // this protects from multiple time executions when Global and Attribute based configurations are used at the same time. if (httpContext.Items.ContainsKey(nameof(PagesRenderMiddleware))) { throw new ApplicationException(Resources.Exception_PagesRenderMiddlewareAlreadyRegistered); @@ -82,11 +82,35 @@ public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewCompo await next(httpContext).ConfigureAwait(false); } - private static bool IsEditingRequest(HttpContext context) + private bool IsValidEditingRequest(HttpContext context) { - if (context.Request.Query.TryGetValue("mode", out var mode)) + if (context.Request.Path == options.RenderEndpoint) { - return mode == "edit"; + return false; + } + + if (!context.Request.Query.TryGetValue("mode", out var mode) || mode != "edit") + { + return false; + } + + if (!IsValidEditingSecret(context.Request)) + { + return false; + } + + return true; + } + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == options.EditingSecret) + { + return true; + } } return false; From 4570f442f94ab182e0ab7f1af2d6902d477ed2dc Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 25 Mar 2025 16:02:40 +1100 Subject: [PATCH 22/38] Moved Dictionary functionality out of PagesGQLHandler into dedicated DictionaryService --- .../Configuration/PagesOptions.cs | 5 + .../PagesAppConfigurationExtensions.cs | 2 + .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 3 + .../GraphQL/EditingLayoutQueryResponse.cs | 4 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 104 +--- .../Request/Handlers/GraphQL/Site.cs | 2 +- .../Request/Handlers/GraphQL/SiteInfo.cs | 2 +- .../Services/DictionaryService.cs | 87 +++ .../Services/IDictionaryService.cs | 20 + .../Request/Handlers/GraphQL/Constants.cs | 83 +-- .../GraphQLEditingServiceHandlerFixture.cs | 507 +++++++++--------- .../Services/Constants.cs | 79 +++ .../Services/DictionaryServiceFixture.cs | 123 +++++ 14 files changed, 608 insertions(+), 422 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs create mode 100644 src/Sitecore.AspNetCore.SDK.Pages/Services/IDictionaryService.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs index 5ceab69..0108ae8 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -34,4 +34,9 @@ public class PagesOptions /// Gets or sets the Editing Secret. /// public string? EditingSecret { get; set; } = string.Empty; + + /// + /// Gets or sets the number of entries per page in a dictionary. The default value is set to 1000. + /// + public int DictionaryPageSize { get; set; } = 1000; } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index f19395b..ff546e3 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -10,6 +10,7 @@ using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Middleware; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Services; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; @@ -75,6 +76,7 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe services.AddSingleton(); services.AddSingleton(new GraphQLClientFactory(contextId)); + services.AddSingleton(); if (options != null) { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs index 66727cc..9ed28bb 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs @@ -149,5 +149,14 @@ internal static string Exception_PagesRenderMiddlewareAlreadyRegistered { return ResourceManager.GetString("Exception_PagesRenderMiddlewareAlreadyRegistered", resourceCulture); } } + + /// + /// Looks up a localized string similar to GraphQLEditingServiceHandler.HandleEditingLayoutRequest: Response is null, unable to process EditingResponse. + /// + internal static string Exception_UableToProcessEditingResponse { + get { + return ResourceManager.GetString("Exception_UableToProcessEditingResponse", resourceCulture); + } + } } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx index 38d69a8..fb4411d 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx @@ -147,4 +147,7 @@ EditingScriptsTagHelper: Sitecore RenderingContext is Null + + GraphQLEditingServiceHandler.HandleEditingLayoutRequest: Response is null, unable to process EditingResponse + \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs index 30a4ca3..c46aec4 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc.Formatters; +using System.Text.Json.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -16,5 +16,5 @@ public class EditingLayoutQueryResponse /// /// Gets or sets the Site for the Editing Layout Response. /// - public Site? Site { get; set; } + public Site Site { get; set; } = new(); } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 82dc9b0..f2332fa 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -12,6 +12,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Properties; +using Sitecore.AspNetCore.SDK.Pages.Services; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -20,16 +21,19 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// Initializes a new instance of the class. /// /// The to use for logging. +/// DictionaryService used to return all dictionary items for a Sitecore site. /// The GraphQlClientFactory used to generate instances of the GraphQl client. /// The serializer to handle response data. public partial class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, ISitecoreLayoutSerializer serializer, - ILogger logger) + ILogger logger, + IDictionaryService dictionaryService) : ILayoutRequestHandler { private readonly IGraphQLClientFactory clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); private readonly ISitecoreLayoutSerializer serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IDictionaryService dictionaryService = dictionaryService ?? throw new ArgumentNullException(nameof(dictionaryService)); /// public async Task Request(SitecoreLayoutRequest request, string handlerName) @@ -263,101 +267,27 @@ private static string GetRequestArgValue(SitecoreLayoutRequest request, string a return headers[argName].FirstOrDefault() ?? string.Empty; } - private static async Task GetFullDictionaryInformation(SitecoreLayoutRequest request, string requestLanguage, IGraphQLClient client, GraphQLResponse response) - { - var hasNext = response.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false; - var endCursor = response.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty; - while (hasNext && endCursor != string.Empty) - { - GraphQLRequest dictionaryRequest = BuildEditingDictionaryRequest(request, requestLanguage, endCursor); - - GraphQLResponse dictionaryResponse = await client.SendQueryAsync(dictionaryRequest).ConfigureAwait(false); - response.Data?.Site?.SiteInfo?.Dictionary?.Results.AddRange(dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.Results ?? []); - - hasNext = dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false; - endCursor = dictionaryResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty; - } - } - - private static GraphQLRequest BuildEditingDictionaryRequest(SitecoreLayoutRequest request, string requestLanguage, string endCursor) - { - return new() - { - Query = @" - query EditingDictionaryQuery( - $siteName: String! - $language: String! - $after: String - $pageSize: Int - ) { - site { - siteInfo(site: $siteName) { - dictionary(language: $language, first: $pageSize, after: $after) { - results { - key - value - } - pageInfo { - endCursor - hasNext - } - } - } - } - } - ", - OperationName = "EditingDictionaryQuery", - Variables = new - { - language = requestLanguage, - siteName = request.SiteName(), - pageSize = 10, - after = endCursor - } - }; - } - private static GraphQLRequest BuildEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage) { return new() { Query = @" query EditingQuery( - $siteName: String!, - $itemId: String!, - $language: String!, - $version: String, - $after: String, - $pageSize: Int - ) { - item(path: $itemId, language: $language, version: $version) { - rendered - } - site { - siteInfo(site: $siteName) { - dictionary(language: $language, first: $pageSize, after: $after) { - results { - key - value - } - pageInfo { - endCursor - hasNext - } - } - } - } - } + $itemId: String!, + $language: String!, + $version: String + ) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + } ", OperationName = "EditingQuery", Variables = new { itemId = GetRequestArgValue(request, "sc_itemid"), language = requestLanguage, - siteName = request.SiteName(), - version = GetRequestArgValue(request, "sc_version"), - pageSize = 10, - after = string.Empty + version = GetRequestArgValue(request, "sc_version") } }; } @@ -366,8 +296,12 @@ query EditingQuery( { IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode") == "edit"); GraphQLResponse response = await client.SendQueryAsync(BuildEditingLayoutRequest(request, requestLanguage)).ConfigureAwait(false); + if (response?.Data == null) + { + throw new Exception(Resources.Exception_UableToProcessEditingResponse); + } - await GetFullDictionaryInformation(request, requestLanguage, client, response).ConfigureAwait(false); + response.Data.Site.SiteInfo.Dictionary.Results = await dictionaryService.GetSiteDictionary(request.SiteName() ?? string.Empty, requestLanguage, client); if (logger.IsEnabled(LogLevel.Debug)) { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs index 2e738bb..4d9061e 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/Site.cs @@ -8,5 +8,5 @@ public class Site /// /// Gets or sets the site info. /// - public SiteInfo? SiteInfo { get; set; } + public SiteInfo SiteInfo { get; set; } = new(); } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs index fedbf41..45f540e 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/SiteInfo.cs @@ -8,5 +8,5 @@ public class SiteInfo /// /// Gets or sets the dictionary for a Sitecore Site. /// - public SiteInfoDictionary? Dictionary { get; set; } + public SiteInfoDictionary Dictionary { get; set; } = new(); } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs new file mode 100644 index 0000000..9e8e1e8 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs @@ -0,0 +1,87 @@ +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Options; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Services; + +/// +/// DictionaryService used retrieve dictionary items for a Sitecore site. +/// +public class DictionaryService(IOptions options) : IDictionaryService +{ + private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + + /// + /// Retrieves a list of site information dictionary items based on the specified site and language. + /// + /// Specifies the name of the site for which the dictionary items are being retrieved. + /// Indicates the language in which the dictionary items should be returned. + /// Represents the GraphQL client used to send requests and receive responses. + /// Returns a list of site information dictionary items. + public async Task> GetSiteDictionary(string siteName, string requestLanguage, IGraphQLClient client) + { + if (string.IsNullOrWhiteSpace(siteName) || string.IsNullOrWhiteSpace(requestLanguage) || client == null) + { + throw new ArgumentNullException(nameof(siteName)); + } + + List dictionary = new(); + GraphQLResponse dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, string.Empty).ConfigureAwait(false); + + while (dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false + && (dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty) != string.Empty) + { + dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty).ConfigureAwait(false); + } + + return dictionary; + } + + private async Task> GetSinglePageOfDictionaryItems(string siteName, string requestLanguage, IGraphQLClient client, List dictionary, string endCursor) + { + GraphQLRequest dictionaryPageRequest = BuildEditingDictionaryRequest(siteName, requestLanguage, endCursor); + GraphQLResponse dictionaryPageResponse = await client.SendQueryAsync(dictionaryPageRequest).ConfigureAwait(false); + dictionary.AddRange(dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.Results ?? []); + return dictionaryPageResponse; + } + + private GraphQLRequest BuildEditingDictionaryRequest(string siteName, string requestLanguage, string endCursor) + { + return new() + { + Query = @" + query DictionaryQuery( + $siteName: String! + $language: String! + $after: String + $pageSize: Int + ) { + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + ", + OperationName = "DictionaryQuery", + Variables = new + { + language = requestLanguage, + siteName = siteName, + pageSize = options.DictionaryPageSize, + after = endCursor + } + }; + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Services/IDictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/IDictionaryService.cs new file mode 100644 index 0000000..363493d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/IDictionaryService.cs @@ -0,0 +1,20 @@ +using GraphQL.Client.Abstractions; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Services +{ + /// + /// DictionaryService used retrieve dictionary items for a Sitecore site. + /// + public interface IDictionaryService + { + /// + /// Retrieves a list of site information based on the specified site and language. + /// + /// Specifies the name of the site for which information is being requested. + /// Indicates the language in which the site information should be returned. + /// Represents the GraphQL client used to make the request for site information. + /// Returns a task that resolves to a list of site information dictionary items. + Task> GetSiteDictionary(string siteName, string requestLanguage, IGraphQLClient client); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs index e2181c7..5bab454 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs @@ -1,10 +1,10 @@ -using GraphQL; +using System.Text.Json; +using GraphQL; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response; using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; -using System.Text.Json; namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL { @@ -21,19 +21,6 @@ public static GraphQLResponse SimpleEditingLayoutQue Item = new ItemModel { Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement - }, - Site = new Pages.Request.Handlers.GraphQL.Site - { - SiteInfo = new SiteInfo - { - Dictionary = new SiteInfoDictionary - { - PageInfo = new PageInfo - { - HasNext = false - } - } - } } } }; @@ -51,59 +38,6 @@ public static GraphQLResponse EditingLayoutQueryResp Item = new ItemModel { Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement - }, - Site = new Pages.Request.Handlers.GraphQL.Site - { - SiteInfo = new SiteInfo - { - Dictionary = new SiteInfoDictionary - { - PageInfo = new PageInfo - { - HasNext = true, - EndCursor = "cursor_value_1234" - } - } - } - } - } - }; - } - } - - public static GraphQLResponse EditingDictionaryResponse - { - get - { - return new GraphQLResponse - { - Data = new EditingDictionaryResponse - { - Site = new Pages.Request.Handlers.GraphQL.Site - { - SiteInfo = new SiteInfo - { - Dictionary = new SiteInfoDictionary - { - Results = new List - { - new SiteInfoDictionaryItem - { - Key = "key1", - Value = "value1" - }, - new SiteInfoDictionaryItem - { - Key = "key2", - Value = "value2" - } - }, - PageInfo = new PageInfo - { - HasNext = false - } - } - } } } }; @@ -121,19 +55,6 @@ public static GraphQLResponse MockEditingLayoutQuery Item = new ItemModel { Rendered = JsonDocument.Parse(@"{ ""sitecore"" : {}}").RootElement - }, - Site = new Pages.Request.Handlers.GraphQL.Site - { - SiteInfo = new SiteInfo - { - Dictionary = new SiteInfoDictionary - { - PageInfo = new PageInfo - { - HasNext = false - } - } - } } } }; diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 63905ff..965600a 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -16,286 +16,289 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Services; using Xunit; -namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL; + +public class GraphQLEditingServiceHandlerFixture { - public class GraphQLEditingServiceHandlerFixture + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => { - [ExcludeFromCodeCoverage] - public static Action AutoSetup => f => - { - IGraphQLClient client = Substitute.For(); - f.Inject(client); + IGraphQLClient client = Substitute.For(); + f.Inject(client); - IGraphQLClientFactory clientFactory = Substitute.For(); - f.Inject(clientFactory); + IGraphQLClientFactory clientFactory = Substitute.For(); + f.Inject(clientFactory); - ISitecoreLayoutSerializer mockSerializer = Substitute.For(); - f.Inject(mockSerializer); + ISitecoreLayoutSerializer mockSerializer = Substitute.For(); + f.Inject(mockSerializer); - SitecoreLayoutRequest request = new() + IDictionaryService mockDictionaryService = Substitute.For(); + f.Inject(mockDictionaryService); + + SitecoreLayoutRequest request = new() + { { + "sc_request_headers_key" , new Dictionary() { - "sc_request_headers_key" , new Dictionary() - { - { "mode", ["edit"] }, - { "language", ["en"] }, - { "sc_layoutKind", ["Final"] }, - { "sc_itemid", ["item_1234"] }, - { "sc_version", ["version_1234"] } - } - }, - { - "sc_lang", "en" - }, - { - "sc_site", "site_1234" + { "mode", ["edit"] }, + { "language", ["en"] }, + { "sc_layoutKind", ["Final"] }, + { "sc_itemid", ["item_1234"] }, + { "sc_version", ["version_1234"] } } - }; - f.Inject(request); + }, + { + "sc_lang", "en" + }, + { + "sc_site", "site_1234" + } }; + f.Inject(request); + }; - [Theory] - [AutoNSubstituteData] - public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) - { - guard.VerifyConstructors(); - } + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_RequestParamIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) - { - // Act - Func act = async () => { await sut.Request(null!, string.Empty); }; + [Theory] + [AutoNSubstituteData] + public async Task Request_RequestParamIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request(null!, string.Empty); }; - // Assert - await act.Should().ThrowAsync(); - } + // Assert + await act.Should().ThrowAsync(); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_HandlerNameIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) - { - // Act - Func act = async () => { await sut.Request([], null!); }; + [Theory] + [AutoNSubstituteData] + public async Task Request_HandlerNameIsNull_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request([], null!); }; - // Assert - await act.Should().ThrowAsync(); - } + // Assert + await act.Should().ThrowAsync(); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_HandlerNameIsEmptyString_ErrorThrown(GraphQLEditingServiceHandler sut) - { - // Act - Func act = async () => { await sut.Request([], string.Empty); }; + [Theory] + [AutoNSubstituteData] + public async Task Request_HandlerNameIsEmptyString_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Act + Func act = async () => { await sut.Request([], string.Empty); }; - // Assert - await act.Should().ThrowAsync(); - } + // Assert + await act.Should().ThrowAsync(); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_NotValidEditingRequest_ErrorThrown(GraphQLEditingServiceHandler sut) - { - // Arrange - SitecoreLayoutRequest request = []; + [Theory] + [AutoNSubstituteData] + public async Task Request_NotValidEditingRequest_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Arrange + SitecoreLayoutRequest request = []; - // Act - Func act = async () => { await sut.Request(request, "editingHandler"); }; + // Act + Func act = async () => { await sut.Request(request, "editingHandler"); }; - // Assert - await act.Should().ThrowAsync().WithMessage("GraphQLEditingServiceHandler: Error attempting to process non-editing request"); - } + // Assert + await act.Should().ThrowAsync().WithMessage("GraphQLEditingServiceHandler: Error attempting to process non-editing request"); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler sut) + [Theory] + [AutoNSubstituteData] + public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler sut) + { + // Arrange + SitecoreLayoutRequest request = new SitecoreLayoutRequest { - // Arrange - SitecoreLayoutRequest request = new SitecoreLayoutRequest { + "sc_request_headers_key" , new Dictionary() { - "sc_request_headers_key" , new Dictionary() - { - { "mode", ["edit"] } - } + { "mode", ["edit"] } } - }; + } + }; - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - // Assert - result.Errors.Should().ContainItemsAssignableTo(); - } + // Assert + result.Errors.Should().ContainItemsAssignableTo(); + } - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.SimpleEditingLayoutQueryResponse); - GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - await client.Received(1).SendQueryAsync(Arg.Any()); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_DictionaryRequestMade(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingLayoutQueryResponseWithDictionaryPaging); - client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingDictionaryResponse); - GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Asset - result.Errors.Should().BeEmpty(); - await client.Received(1).SendQueryAsync(Arg.Any()); - await client.Received(1).SendQueryAsync(Arg.Any()); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); - mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_Placeholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(2); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["id"].Should().Be($"placeholder_1_{Guid.Empty.ToString()}"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); - mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_NestedPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["id"].Should().Be("container-{*}_component_1"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); - mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_WithComponentInPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["id"].Should().Be($"component_1"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); - mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentInNestedPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["id"].Should().Be($"nested_component_2"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].Should().BeOfType(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); - } - - [Theory] - [AutoNSubstituteData] - public async Task Request_ValidRequest_FieldRenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) - { - // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); - client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); - mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentWithField); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>()); - - // Act - SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); - - // Assert - result.Errors.Should().BeEmpty(); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields.Values.Count.Should().Be(1); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"].Should().BeOfType(); - JsonSerializedField? jsonSerialisedField = result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"] as JsonSerializedField; - jsonSerialisedField.Should().NotBeNull(); - EditableField? editableField = jsonSerialisedField?.Read>(); - editableField.Should().NotBeNull(); - editableField?.OpeningChrome.Should().NotBeNull(); - editableField?.OpeningChrome?.Attributes["chrometype"].Should().Be("field"); - editableField?.OpeningChrome?.Attributes["kind"].Should().Be("open"); - editableField?.OpeningChrome?.Content.Should().Be(@"{""datasource"":{""id"":""datasource_id"",""language"":""en"",""revision"":""revision_1"",""version"":1},""title"":""Text"",""fieldId"":""field_id"",""fieldType"":""Text"",""rawValue"":""field_raw_value""}"); - editableField?.ClosingChrome.Should().NotBeNull(); - editableField?.ClosingChrome?.Attributes["chrometype"].Should().Be("field"); - editableField?.ClosingChrome?.Attributes["kind"].Should().Be("close"); - } + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.SimpleEditingLayoutQueryResponse); + GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + await client.Received(1).SendQueryAsync(Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_DictionaryServiceIsCalled(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request, IDictionaryService mockDictionaryService) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingLayoutQueryResponseWithDictionaryPaging); + + GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>(), mockDictionaryService); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Asset + result.Errors.Should().BeEmpty(); + await client.Received(1).SendQueryAsync(Arg.Any()); + await mockDictionaryService.Received(1).GetSiteDictionary(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_Placeholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(2); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][0].As().Attributes["id"].Should().Be($"placeholder_1_{Guid.Empty.ToString()}"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_NestedPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["id"].Should().Be("container-{*}_component_1"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_WithComponentInPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][1].As().Attributes["id"].Should().Be($"component_1"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentInNestedPlaceholder); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("open"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["id"].Should().Be($"nested_component_2"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].Should().BeOfType(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["chrometype"].Should().Be("rendering"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][3].As().Attributes["kind"].Should().Be("close"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_FieldRenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentWithField); + GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + + // Act + SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); + + // Assert + result.Errors.Should().BeEmpty(); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"].Count.Should().Be(5); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields.Values.Count.Should().Be(1); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"].Should().BeOfType(); + JsonSerializedField? jsonSerialisedField = result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Fields["field_1"] as JsonSerializedField; + jsonSerialisedField.Should().NotBeNull(); + EditableField? editableField = jsonSerialisedField?.Read>(); + editableField.Should().NotBeNull(); + editableField?.OpeningChrome.Should().NotBeNull(); + editableField?.OpeningChrome?.Attributes["chrometype"].Should().Be("field"); + editableField?.OpeningChrome?.Attributes["kind"].Should().Be("open"); + editableField?.OpeningChrome?.Content.Should().Be(@"{""datasource"":{""id"":""datasource_id"",""language"":""en"",""revision"":""revision_1"",""version"":1},""title"":""Text"",""fieldId"":""field_id"",""fieldType"":""Text"",""rawValue"":""field_raw_value""}"); + editableField?.ClosingChrome.Should().NotBeNull(); + editableField?.ClosingChrome?.Attributes["chrometype"].Should().Be("field"); + editableField?.ClosingChrome?.Attributes["kind"].Should().Be("close"); } } \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs new file mode 100644 index 0000000..82c21ff --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs @@ -0,0 +1,79 @@ +using GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Services +{ + public static class Constants + { + public static GraphQLResponse DictionaryResponseWithoutPaging + { + get + { + return new GraphQLResponse + { + Data = new EditingDictionaryResponse + { + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + Results = new List + { + new SiteInfoDictionaryItem + { + Key = "key1", + Value = "value1" + } + }, + PageInfo = new PageInfo + { + HasNext = false, + EndCursor = string.Empty + } + } + } + } + } + }; + } + } + + public static GraphQLResponse DictionaryResponseWithPaging + { + get + { + return new GraphQLResponse + { + Data = new EditingDictionaryResponse + { + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + Results = new List + { + new SiteInfoDictionaryItem + { + Key = "page1", + Value = "page1" + } + }, + PageInfo = new PageInfo + { + HasNext = true, + EndCursor = "abcd1234" + } + } + } + } + } + }; + } + } + + } +} diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs new file mode 100644 index 0000000..c6d7c32 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs @@ -0,0 +1,123 @@ +using System.Diagnostics.CodeAnalysis; +using AutoFixture; +using AutoFixture.Idioms; +using FluentAssertions; +using GraphQL; +using GraphQL.Client.Abstractions; +using Microsoft.Extensions.Options; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Services; + +public class DictionaryServiceFixture +{ + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + IOptions pagesOptions = Substitute.For>(); + pagesOptions.Value.Returns(new PagesOptions()); + f.Inject(pagesOptions); + }; + + [Theory] + [AutoNSubstituteData] + public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) + { + guard.VerifyConstructors(); + } + + [Theory] + [AutoNSubstituteData] + public async Task GetSiteDictionary_SiteNameIsNull_ErrorThrown(DictionaryService sut) + { + // Act + Func act = async () => { await sut.GetSiteDictionary(string.Empty, null!, null!); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task GetSiteDictionary_LanguageIsNull_ErrorThrown(DictionaryService sut) + { + // Act + Func act = async () => { await sut.GetSiteDictionary("valid_site", null!, null!); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task GetSiteDictionary_ClientIsNull_ErrorThrown(DictionaryService sut) + { + // Act + Func act = async () => { await sut.GetSiteDictionary("valid_site", "valid_language", null!); }; + + // Assert + await act.Should().ThrowAsync(); + } + + [Theory] + [AutoNSubstituteData] + public async Task GetSiteDictionary_SinglePageResults_ReturnsCorrectCollection(IOptions pageOptions) + { + // Arrange + DictionaryService sut = new(pageOptions); + IGraphQLClient graphQLClient = Substitute.For(); + graphQLClient.SendQueryAsync(Arg.Any()).Returns(Constants.DictionaryResponseWithoutPaging); + + // Act + List result = await sut.GetSiteDictionary("valid_site", "valid_language", graphQLClient); + + // Assert + result.Should().HaveCount(1); + } + + [Theory] + [AutoNSubstituteData] + public async Task GetSiteDictionary_MultiplePageResult_ReturnsCorrectCollection(IOptions pageOptions) + { + // Arrange + DictionaryService sut = new(pageOptions); + IGraphQLClient graphQLClient = Substitute.For(); + graphQLClient.SendQueryAsync(Arg.Is(x => GraphQlQueryHasAfterVariableWithValue(x, string.Empty))).Returns(Constants.DictionaryResponseWithPaging); + graphQLClient.SendQueryAsync(Arg.Is(x => GraphQlQueryHasAfterVariableWithValue(x, "abcd1234"))).Returns(Constants.DictionaryResponseWithoutPaging); + + // Act + List result = await sut.GetSiteDictionary("valid_site", "valid_language", graphQLClient); + + // Assert + result.Should().HaveCount(2); + } + + public bool GraphQlQueryHasAfterVariableWithValue(GraphQLRequest graphQlRequst, string expectedAfterValue) + { + if (!graphQlRequst.ContainsKey("variables")) + { + return false; + } + + Type afterVariable = graphQlRequst["variables"].GetType(); + var afterProperty = afterVariable.GetProperty("after"); + if (afterProperty == null) + { + return false; + } + + var afterVariableValue = afterProperty.GetValue(graphQlRequst["variables"]); + if (afterVariableValue == null) + { + return false; + } + + return afterVariableValue.ToString() == expectedAfterValue; + } +} From fa5202e2ba52015ca291fc2a6394fd5dcfd93046 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 26 Mar 2025 13:35:26 +1100 Subject: [PATCH 23/38] Removed GraphQlClient used for EditingHandler, added GraphQLClient with customisable HeaderCollection instead --- .../Request/GraphQLHttpRequestWithHeaders.cs | 36 +++++++++ .../PagesAppConfigurationExtensions.cs | 7 +- .../GraphQL/GraphQLClientFactory.cs | 34 -------- .../GraphQL/IGraphQLClientFactory.cs | 26 ------ .../GraphQL/GraphQLEditingServiceHandler.cs | 17 ++-- .../GraphQLHttpRequestWithHeadersFixture.cs | 44 +++++++++++ .../GraphQL/GraphQLClientFactoryFixture.cs | 79 ------------------- .../GraphQLEditingServiceHandlerFixture.cs | 39 ++++----- 8 files changed, 108 insertions(+), 174 deletions(-) create mode 100644 src/Sitecore.AspNetCore.SDK.GraphQL/Request/GraphQLHttpRequestWithHeaders.cs delete mode 100644 src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs delete mode 100644 src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs delete mode 100644 tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Request/GraphQLHttpRequestWithHeaders.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Request/GraphQLHttpRequestWithHeaders.cs new file mode 100644 index 0000000..eafe35e --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Request/GraphQLHttpRequestWithHeaders.cs @@ -0,0 +1,36 @@ +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; + +namespace Sitecore.AspNetCore.SDK.GraphQL.Request +{ + /// + /// GraphQLHttpRequestWithHeaders is a class that extends GraphQLHttpRequest. It is designed to handle GraphQL HTTP + /// requests with additional header support. + /// + public class GraphQLHttpRequestWithHeaders : GraphQLHttpRequest + { + /// + /// Gets or sets a dictionary that stores key-value pairs of headers, where both keys and values are strings. It allows for + /// easy access and manipulation of header information. + /// + public Dictionary Headers { get; set; } = []; + + /// + /// Converts the current instance into an HTTP request message for GraphQL operations. + /// + /// Specifies configuration options for the GraphQL HTTP client. + /// Defines the method for serializing GraphQL requests and responses. + /// Returns an HTTP request message with the necessary headers for the GraphQL operation. + public override HttpRequestMessage ToHttpRequestMessage(GraphQLHttpClientOptions options, IGraphQLJsonSerializer serializer) + { + HttpRequestMessage request = base.ToHttpRequestMessage(options, serializer); + + foreach (string headerKey in Headers.Keys) + { + request.Headers.Add(headerKey, Headers[headerKey]); + } + + return request; + } + } +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index ff546e3..693bba1 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Builder; +using GraphQL.Client.Abstractions; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -7,7 +8,6 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.Pages.Configuration; -using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Middleware; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Services; @@ -75,7 +75,6 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe } services.AddSingleton(); - services.AddSingleton(new GraphQLClientFactory(contextId)); services.AddSingleton(); if (options != null) @@ -114,7 +113,7 @@ public static ISitecoreLayoutClientBuilder AddSitecorePagesHandler( builder.AddHandler(name, sp => ActivatorUtilities.CreateInstance( sp, - sp.GetRequiredService(), + sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>())); diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs deleted file mode 100644 index 3e4f0a7..0000000 --- a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/GraphQLClientFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using GraphQL.Client.Abstractions; -using GraphQL.Client.Http; -using GraphQL.Client.Serializer.SystemTextJson; -using Sitecore.AspNetCore.SDK.GraphQL.Extensions; - -namespace Sitecore.AspNetCore.SDK.Pages.GraphQL; - -/// -/// GraphQLClientFactory used to generate instances of of GraphQLClients authenticated using a ContextId. -/// The contextId for the envionment being used. -/// -public class GraphQLClientFactory(string contextId) - : IGraphQLClientFactory -{ - private readonly string contextId = contextId ?? throw new ArgumentNullException(nameof(contextId)); - - /// - public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, bool editMode) - { - uri ??= new Uri("https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1"); - uri = uri.AddQueryString("sitecoreContextId", contextId)!; - - GraphQLHttpClient client = new(uri, new SystemTextJsonSerializer()); - client.HttpClient.DefaultRequestHeaders.Add("sc_layoutKind", layoutKind); - client.HttpClient.DefaultRequestHeaders.Add("sc_editmode", editMode.ToString()); - return client; - } - - /// - public IGraphQLClient GenerateClient(string layoutKind, bool editMode) - { - return GenerateClient(null, layoutKind, editMode); - } -} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs b/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs deleted file mode 100644 index 7e91061..0000000 --- a/src/Sitecore.AspNetCore.SDK.Pages/GraphQL/IGraphQLClientFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -using GraphQL.Client.Abstractions; - -namespace Sitecore.AspNetCore.SDK.Pages.GraphQL; - -/// -/// Interface used to define the contract that IGraphQlClientFactories need to adhere to. -/// -public interface IGraphQLClientFactory -{ - /// - /// Method used to generate an instance of . - /// - /// GraphQl endpoint uri. - /// The layout type for this request, shared or final. - /// The edit mode version for this client. - /// Concrete implementation of interface. - public IGraphQLClient GenerateClient(Uri? uri, string layoutKind, bool editMode); - - /// - /// Method used to generate an instance of . - /// - /// The layout type for this request, shared or final. - /// The edit mode version for this client. - /// Concrete implementation of interface. - public IGraphQLClient GenerateClient(string layoutKind, bool editMode); -} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index f2332fa..ac60358 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -2,6 +2,7 @@ using GraphQL; using GraphQL.Client.Abstractions; using Microsoft.Extensions.Logging; +using Sitecore.AspNetCore.SDK.GraphQL.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; using Sitecore.AspNetCore.SDK.LayoutService.Client.Request; @@ -10,7 +11,6 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; -using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.Pages.Services; @@ -20,17 +20,17 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// /// Initializes a new instance of the class. /// +/// The GraphQL Client used for requests /// The to use for logging. /// DictionaryService used to return all dictionary items for a Sitecore site. -/// The GraphQlClientFactory used to generate instances of the GraphQl client. /// The serializer to handle response data. -public partial class GraphQLEditingServiceHandler(IGraphQLClientFactory clientFactory, +public partial class GraphQLEditingServiceHandler(IGraphQLClient client, ISitecoreLayoutSerializer serializer, ILogger logger, IDictionaryService dictionaryService) : ILayoutRequestHandler { - private readonly IGraphQLClientFactory clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + private readonly IGraphQLClient client = client ?? throw new ArgumentNullException(nameof(client)); private readonly ISitecoreLayoutSerializer serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IDictionaryService dictionaryService = dictionaryService ?? throw new ArgumentNullException(nameof(dictionaryService)); @@ -267,7 +267,7 @@ private static string GetRequestArgValue(SitecoreLayoutRequest request, string a return headers[argName].FirstOrDefault() ?? string.Empty; } - private static GraphQLRequest BuildEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage) + private static GraphQLHttpRequestWithHeaders BuildEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage) { return new() { @@ -288,14 +288,19 @@ query EditingQuery( itemId = GetRequestArgValue(request, "sc_itemid"), language = requestLanguage, version = GetRequestArgValue(request, "sc_version") + }, + Headers = new Dictionary + { + { "sc_layoutKind", GetRequestArgValue(request, "sc_layoutKind") }, + { "sc_editmode", (GetRequestArgValue(request, "mode") == "edit").ToString() } } }; } private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) { - IGraphQLClient client = clientFactory.GenerateClient(GetRequestArgValue(request, "sc_layoutKind"), GetRequestArgValue(request, "mode") == "edit"); GraphQLResponse response = await client.SendQueryAsync(BuildEditingLayoutRequest(request, requestLanguage)).ConfigureAwait(false); + if (response?.Data == null) { throw new Exception(Resources.Exception_UableToProcessEditingResponse); diff --git a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs new file mode 100644 index 0000000..bdd4263 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using AutoFixture; +using FluentAssertions; +using GraphQL.Client.Abstractions; +using GraphQL.Client.Http; +using NSubstitute; +using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; +using Sitecore.AspNetCore.SDK.GraphQL.Request; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.GraphQL.Tests.Request; + +public class GraphQLHttpRequestWithHeadersFixture +{ + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + GraphQLHttpClientOptions options = Substitute.For (); + f.Inject(options); + + IGraphQLJsonSerializer serializer = Substitute.For(); + f.Inject(serializer); + }; + + [Theory] + [AutoNSubstituteData] + public void ToHttpRequestMessage_HeadersAreAddedToReturnedMessage(GraphQLHttpRequestWithHeaders sut, GraphQLHttpClientOptions options, IGraphQLJsonSerializer serializer) + { + // Arrange + sut.Headers = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + // Act + HttpRequestMessage result = sut.ToHttpRequestMessage(options, serializer); + + // Assert + result.Should().NotBeNull(); + result.Headers.GetValues("key1").Should().Contain("value1"); + result.Headers.GetValues("key2").Should().Contain("value2"); + } +} diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs deleted file mode 100644 index a36db2c..0000000 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/GraphQL/GraphQLClientFactoryFixture.cs +++ /dev/null @@ -1,79 +0,0 @@ -using AutoFixture.Idioms; -using FluentAssertions; -using GraphQL.Client.Http; -using Sitecore.AspNetCore.SDK.AutoFixture.Attributes; -using Sitecore.AspNetCore.SDK.AutoFixture.Extensions; -using Sitecore.AspNetCore.SDK.Pages.GraphQL; -using Xunit; - -namespace Sitecore.AspNetCore.SDK.Pages.Tests.GraphQL -{ - public class GraphQLClientFactoryFixture - { - [Theory] - [AutoNSubstituteData] - public void Ctor_InvalidArgs_Throws(GuardClauseAssertion guard) - { - guard.VerifyConstructors(); - } - - [Theory] - [AutoNSubstituteData] - public void GenerateClient_NullUriDefaultsToEdgePlatformUri(GraphQLClientFactory sut) - { - // Act - var result = sut.GenerateClient(null, string.Empty, false); - - // Assert - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("https://edge-platform.sitecorecloud.io/v1/content/api/graphql/v1"); - } - - [Theory] - [AutoNSubstituteData] - public void GenerateClient_OverriddenUriUsedInClient(GraphQLClientFactory sut) - { - // Act - var result = sut.GenerateClient(new Uri("https://some.domain.com"), string.Empty, false); - - // Assert - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("https://some.domain.com"); - } - - [Theory] - [AutoNSubstituteData] - public void GenerateClient_ContextIdIsAppendedToClientUri(GraphQLClientFactory sut) - { - // Arrange - sut = new GraphQLClientFactory("1234"); - - // Act - var result = sut.GenerateClient(null, string.Empty, false); - - // Assert - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.As().Options.EndPoint?.AbsoluteUri.Should().Contain("sitecoreContextId=1234"); - } - - [Theory] - [AutoNSubstituteData] - public void GenerateClient_CorrectParamsAreSetWhenGeneratingClient(GraphQLClientFactory sut) - { - // Arrange - sut = new GraphQLClientFactory("1234"); - - // Act - var result = sut.GenerateClient(null, "layout_kid_1234", true); - - // Assert - result.Should().NotBeNull(); - result.Should().BeOfType(); - result.As().HttpClient.DefaultRequestHeaders.GetValues("sc_layoutKind").Should().Equal(["layout_kid_1234"]); - result.As().HttpClient.DefaultRequestHeaders.GetValues("sc_editmode").Should().Equal([true.ToString()]); - } - } -} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 965600a..19a3a15 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -14,7 +14,6 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; -using Sitecore.AspNetCore.SDK.Pages.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; using Sitecore.AspNetCore.SDK.Pages.Services; using Xunit; @@ -29,9 +28,6 @@ public class GraphQLEditingServiceHandlerFixture IGraphQLClient client = Substitute.For(); f.Inject(client); - IGraphQLClientFactory clientFactory = Substitute.For(); - f.Inject(clientFactory); - ISitecoreLayoutSerializer mockSerializer = Substitute.For(); f.Inject(mockSerializer); @@ -138,12 +134,11 @@ public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClient client, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.SimpleEditingLayoutQueryResponse); - GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, Substitute.For(), Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -155,13 +150,12 @@ public async Task Request_ValidRequest_NoErrorsThrown(IGraphQLClientFactory clie [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_DictionaryServiceIsCalled(IGraphQLClientFactory clientFactory, IGraphQLClient client, SitecoreLayoutRequest request, IDictionaryService mockDictionaryService) + public async Task Request_ValidRequest_DictionaryServiceIsCalled(IGraphQLClient client, SitecoreLayoutRequest request, IDictionaryService mockDictionaryService) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingLayoutQueryResponseWithDictionaryPaging); - GraphQLEditingServiceHandler sut = new(clientFactory, Substitute.For(), Substitute.For>(), mockDictionaryService); + GraphQLEditingServiceHandler sut = new(client, Substitute.For(), Substitute.For>(), mockDictionaryService); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -174,13 +168,12 @@ public async Task Request_ValidRequest_DictionaryServiceIsCalled(IGraphQLClientF [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_Placeholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, mockSerializer, Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -199,13 +192,12 @@ public async Task Request_ValidRequest_PlaceholderChromesAreAdded(IGraphQLClient [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_NestedPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, mockSerializer, Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -223,13 +215,12 @@ public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQL [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_WithComponentInPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, mockSerializer, Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -248,13 +239,12 @@ public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClientFa [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentInNestedPlaceholder); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, mockSerializer, Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); @@ -273,13 +263,12 @@ public async Task Request_ValidRequest_RenderingInNestedPlaceholderChromesAreAdd [Theory] [AutoNSubstituteData] - public async Task Request_ValidRequest_FieldRenderingChromesAreAdded(IGraphQLClientFactory clientFactory, IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + public async Task Request_ValidRequest_FieldRenderingChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) { // Arrange - clientFactory.GenerateClient(Arg.Any(), Arg.Any()).Returns(client); client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentWithField); - GraphQLEditingServiceHandler sut = new(clientFactory, mockSerializer, Substitute.For>(), Substitute.For()); + GraphQLEditingServiceHandler sut = new(client, mockSerializer, Substitute.For>(), Substitute.For()); // Act SitecoreLayoutResponse result = await sut.Request(request, "editingHandler"); From f8eee28d9ff4bfa3b2de826ff36be98aecc6734e Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 26 Mar 2025 14:02:22 +1100 Subject: [PATCH 24/38] Changed to no longer persist component names for predicate registrations as its not supported by MetaData Editing --- .../Controllers/PagesSetupController.cs | 2 +- .../Extensions/RenderingEngineOptionsExtensions.cs | 2 +- .../Rendering/ComponentRendererDescriptor.cs | 3 ++- .../Rendering/LoggingComponentRenderer.cs | 3 +-- .../Rendering/PartialViewComponentRenderer.cs | 3 +-- .../Rendering/ViewComponentComponentRenderer.cs | 3 +-- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index 345ba0a..e9e45d2 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -91,7 +91,7 @@ private PagesConfigResponse BuildConfigResponseBody() return new PagesConfigResponse { EditMode = "metadata", - Components = renderingEngineOptions.RendererRegistry.Select(x => x.Value.ComponentName).ToList() + Components = renderingEngineOptions.RendererRegistry.Where(x => x.Value.ComponentName != string.Empty).Select(x => x.Value.ComponentName).ToList() }; } diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs index aea0ff5..bbe9951 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs @@ -151,7 +151,7 @@ public static RenderingEngineOptions AddViewComponent( ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); - ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName, viewComponentName); + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs index 335eaea..679ba70 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs @@ -8,10 +8,11 @@ /// /// The predicate to use when retrieving a . /// The factory method to create a new instance of the . +/// The name of the component being added. public class ComponentRendererDescriptor( Predicate match, Func factory, - string componentName) + string componentName = "") { private readonly Func _factory = factory ?? throw new ArgumentNullException(nameof(factory)); private readonly object _lock = new(); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs index f6ddf81..9b48514 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/LoggingComponentRenderer.cs @@ -31,8 +31,7 @@ public static ComponentRendererDescriptor Describe(Predicate match) return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp), - string.Empty); + sp => ActivatorUtilities.CreateInstance(sp)); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs index 777ebf5..1fabce1 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs @@ -35,8 +35,7 @@ public static ComponentRendererDescriptor Describe(Predicate match, stri ArgumentException.ThrowIfNullOrWhiteSpace(locator); return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp, locator), - locator); + sp => ActivatorUtilities.CreateInstance(sp, locator)); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs index de48e2a..174ebf0 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs @@ -31,11 +31,10 @@ public ViewComponentComponentRenderer(string locator) /// The string to use when locating the View Component. /// The string to use describe the name of the components. /// An instance of . - public static ComponentRendererDescriptor Describe(Predicate match, string locator, string componentName) + public static ComponentRendererDescriptor Describe(Predicate match, string locator, string componentName = "") { ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(locator); - ArgumentException.ThrowIfNullOrWhiteSpace(componentName); return new ComponentRendererDescriptor( match, From ac8a9339d1ae65bfea2fc778ab215d6820f97a16 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 26 Mar 2025 14:17:36 +1100 Subject: [PATCH 25/38] Code TidyUp, fixed all warnings --- .../GraphQL/GraphQLEditingServiceHandler.cs | 22 +-- .../Handlers/GraphQL/PlaceholderWorkItem.cs | 72 ++++++---- .../TagHelpers/EditingScriptsTagHelper.cs | 2 +- .../TagHelpers/Fields/LinkTagHelper.cs | 134 +++++++++--------- .../GraphQLHttpRequestWithHeadersFixture.cs | 2 +- .../PagesSetupControllerFixture.cs | 7 +- .../GraphQLEditingServiceHandlerFixture.cs | 4 +- .../Services/Constants.cs | 1 - .../TagHelpers/Fields/LinkTagHelperFixture.cs | 1 - .../Fields/RichTextTagHelperFixture.cs | 1 - .../Fields/TextFieldTagHelperFixture.cs | 1 - 11 files changed, 130 insertions(+), 117 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index ac60358..35982cb 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -20,7 +20,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// /// Initializes a new instance of the class. /// -/// The GraphQL Client used for requests +/// The GraphQL Client used for requests. /// The to use for logging. /// DictionaryService used to return all dictionary items for a Sitecore site. /// The serializer to handle response data. @@ -126,6 +126,7 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde { ProcessField(updatedFields, field); } + component.Fields = updatedFields; // Add the component to the output @@ -150,8 +151,7 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde placeholderValue, processedPlaceholder, component, - placeholderKey - )); + placeholderKey)); // Store the processed placeholder for later assignment component.Placeholders[placeholderKey] = processedPlaceholder; @@ -204,15 +204,15 @@ private static void ProcessField(Dictionary updatedFields, { datasource = new { - id = editableField?.MetaData?.DataSource?.Id, - language = editableField?.MetaData?.DataSource?.Language, - revision = editableField?.MetaData?.DataSource?.Revision, - version = editableField?.MetaData?.DataSource?.Version + id = editableField.MetaData?.DataSource?.Id, + language = editableField.MetaData?.DataSource?.Language, + revision = editableField.MetaData?.DataSource?.Revision, + version = editableField.MetaData?.DataSource?.Version }, - title = editableField?.MetaData?.Title, - fieldId = editableField?.MetaData?.FieldId, - fieldType = editableField?.MetaData?.FieldType, - rawValue = editableField?.MetaData?.RawValue + title = editableField.MetaData?.Title, + fieldId = editableField.MetaData?.FieldId, + fieldType = editableField.MetaData?.FieldType, + rawValue = editableField.MetaData?.RawValue }; editableField.OpeningChrome = GenerateEditableChrome("field", "open", string.Empty, JsonSerializer.Serialize(openingChromeContent)); diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs index 4693c18..69f60d5 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs @@ -2,32 +2,50 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; -public partial class GraphQLEditingServiceHandler +/// +/// Represents a work item with associated features and output, optionally linked to a parent component. +/// +/// Specifies the name of the work item. +/// Defines a unique identifier for the work item. +/// Holds the features associated with the work item. +/// Contains the output related to the work item. +/// Links to a parent component if applicable. +/// Provides an optional key for placeholder identification. +public class PlaceholderWorkItem( + string name, + string id, + Placeholder features, + Placeholder output, + Component? parentComponent = null, + string? placeholderKey = null) { - // Define the work item class outside of the method to avoid nested class issues - private class PlaceholderWorkItem - { - public string Name { get; } - public string Id { get; } - public Placeholder Features { get; } - public Placeholder Output { get; } - public Component ParentComponent { get; } - public string PlaceholderKey { get; } + /// + /// Gets the name of an entity. It is a read-only property initialized with the value of the 'name' variable. + /// + public string Name { get; } = name; - public PlaceholderWorkItem( - string name, - string id, - Placeholder features, - Placeholder output, - Component parentComponent = null, - string placeholderKey = null) - { - Name = name; - Id = id; - Features = features; - Output = output; - ParentComponent = parentComponent; - PlaceholderKey = placeholderKey; - } - } -} \ No newline at end of file + /// + /// Gets the unique identifier as a read-only string property. It is initialized with the value of 'id'. + /// + public string Id { get; } = id; + + /// + /// Gets the collection of features. It is a read-only property that initializes with the value of 'features'. + /// + public Placeholder Features { get; } = features; + + /// + /// Gets the output placeholder. It is a read-only property initialized with the value of 'output'. + /// + public Placeholder Output { get; } = output; + + /// + /// Gets the parent component of the current component. It is a read-only property initialized with the provided parentComponent. + /// + public Component? ParentComponent { get; } = parentComponent; + + /// + /// Gets the read-only property that returns the value of the placeholderKey variable. It is used to access a specific key for placeholders. + /// + public string? PlaceholderKey { get; } = placeholderKey; +} diff --git a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs index 7dff1b0..22e2bba 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -23,7 +23,7 @@ public class EditingScriptsTagHelper : TagHelper public ViewContext? ViewContext { get; set; } /// - public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + public override void Process(TagHelperContext context, TagHelperOutput output) { ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext() ?? throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperSitecoreRenderingContextNull); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs index 64f0d6c..c41d017 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs @@ -75,73 +75,6 @@ public override void Process(TagHelperContext context, TagHelperOutput output) } } - private void RenderMarkup(TagHelperOutput output, HyperLinkField field) - { - if (output.TagName == null) - { - output.Content.SetHtmlContent(GenerateLink(field.Value, output)); - } - else - { - HyperLink hyperLink = field.Value; - - output.Attributes.Add(HrefAttribute, BuildHref(hyperLink)); - - if (!string.IsNullOrWhiteSpace(hyperLink.Target) && !output.Attributes.ContainsName(TargetAttribute)) - { - output.Attributes.Add(TargetAttribute, hyperLink.Target); - } - - if (!string.IsNullOrWhiteSpace(hyperLink.Title) && !output.Attributes.ContainsName(TitleAttribute)) - { - output.Attributes.Add(TitleAttribute, hyperLink.Title); - } - - if (!string.IsNullOrWhiteSpace(hyperLink.Class) && !output.Attributes.ContainsName(ClassAttribute)) - { - output.Attributes.Add(ClassAttribute, hyperLink.Class); - } - - if (hyperLink.Target == BlankValue && !output.Attributes.ContainsName(RelAttribute)) - { - // information disclosure attack prevention keeps target blank site from getting ref to window.opener - output.Attributes.Add(RelAttribute, "noopener noreferrer"); - } - - string? innerContent = output.GetChildContentAsync()?.Result?.GetContent(); - if (string.IsNullOrWhiteSpace(innerContent) && !string.IsNullOrWhiteSpace(hyperLink.Text)) - { - output.Content.Append(field.Value.Text); - } - } - } - - private void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) - { - if (field.OpeningChrome != null && field.ClosingChrome != null) - { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); - - if (field.Value.Href == string.Empty) - { - output.Content.AppendHtml("[No text in field]"); - } - else - { - output.Content.AppendHtml(GenerateLink(field.Value, output)); - } - - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); - } - else - { - DefaultTagHelperContent content = new(); - _ = content.AppendHtml(new HtmlString(field.EditableMarkupFirst)); - _ = content.AppendHtml(new HtmlString(field.EditableMarkupLast)); - output.Content.SetHtmlContent(content); - } - } - /// /// Generates anchor HTML tag. /// @@ -221,4 +154,71 @@ private static string BuildHref(HyperLink hyperLink) return sb.ToString(); } + + private void RenderMarkup(TagHelperOutput output, HyperLinkField field) + { + if (output.TagName == null) + { + output.Content.SetHtmlContent(GenerateLink(field.Value, output)); + } + else + { + HyperLink hyperLink = field.Value; + + output.Attributes.Add(HrefAttribute, BuildHref(hyperLink)); + + if (!string.IsNullOrWhiteSpace(hyperLink.Target) && !output.Attributes.ContainsName(TargetAttribute)) + { + output.Attributes.Add(TargetAttribute, hyperLink.Target); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Title) && !output.Attributes.ContainsName(TitleAttribute)) + { + output.Attributes.Add(TitleAttribute, hyperLink.Title); + } + + if (!string.IsNullOrWhiteSpace(hyperLink.Class) && !output.Attributes.ContainsName(ClassAttribute)) + { + output.Attributes.Add(ClassAttribute, hyperLink.Class); + } + + if (hyperLink.Target == BlankValue && !output.Attributes.ContainsName(RelAttribute)) + { + // information disclosure attack prevention keeps target blank site from getting ref to window.opener + output.Attributes.Add(RelAttribute, "noopener noreferrer"); + } + + string? innerContent = output.GetChildContentAsync()?.Result?.GetContent(); + if (string.IsNullOrWhiteSpace(innerContent) && !string.IsNullOrWhiteSpace(hyperLink.Text)) + { + output.Content.Append(field.Value.Text); + } + } + } + + private void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) + { + if (field.OpeningChrome != null && field.ClosingChrome != null) + { + output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + + if (field.Value.Href == string.Empty) + { + output.Content.AppendHtml("[No text in field]"); + } + else + { + output.Content.AppendHtml(GenerateLink(field.Value, output)); + } + + output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + } + else + { + DefaultTagHelperContent content = new(); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupFirst)); + _ = content.AppendHtml(new HtmlString(field.EditableMarkupLast)); + output.Content.SetHtmlContent(content); + } + } } \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs index bdd4263..054eeeb 100644 --- a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs @@ -15,7 +15,7 @@ public class GraphQLHttpRequestWithHeadersFixture [ExcludeFromCodeCoverage] public static Action AutoSetup => f => { - GraphQLHttpClientOptions options = Substitute.For (); + GraphQLHttpClientOptions options = Substitute.For(); f.Inject(options); IGraphQLJsonSerializer serializer = Substitute.For(); diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs index 039f5bb..aed93f3 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -34,7 +34,7 @@ public class PagesSetupControllerFixture f.Inject(requestDelegate); IOptions pagesOptions = Substitute.For>(); - PagesOptions PagesOptionsValues = new PagesOptions + PagesOptions pagesOptionsValues = new PagesOptions { ConfigEndpoint = ValidConfigEndpoint, RenderEndpoint = ValidRenderEndpoint, @@ -42,13 +42,13 @@ public class PagesSetupControllerFixture ValidOrigins = ValidOrigins, EditingSecret = ValidEditingSecret }; - pagesOptions.Value.Returns(PagesOptionsValues); + pagesOptions.Value.Returns(pagesOptionsValues); f.Inject(pagesOptions); ILogger logger = Substitute.For>(); f.Inject(logger); - IOptions renderingEngineOptions = Substitute.For>(); + IOptions renderingEngineOptions = Substitute.For>(); string componentName = "TestComponent"; ComponentRendererDescriptor componentRendererDescriptor = new(name => name == componentName, _ => null!, componentName); RenderingEngineOptions renderingEngineOptionsValues = new RenderingEngineOptions @@ -57,7 +57,6 @@ public class PagesSetupControllerFixture { { 1, componentRendererDescriptor } } - }; renderingEngineOptions.Value.Returns(renderingEngineOptionsValues); f.Inject(renderingEngineOptions); diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 19a3a15..5505caa 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -37,7 +37,7 @@ public class GraphQLEditingServiceHandlerFixture SitecoreLayoutRequest request = new() { { - "sc_request_headers_key" , new Dictionary() + "sc_request_headers_key", new Dictionary() { { "mode", ["edit"] }, { "language", ["en"] }, @@ -118,7 +118,7 @@ public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler SitecoreLayoutRequest request = new SitecoreLayoutRequest { { - "sc_request_headers_key" , new Dictionary() + "sc_request_headers_key", new Dictionary() { { "mode", ["edit"] } } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs index 82c21ff..f7f1176 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs @@ -74,6 +74,5 @@ public static GraphQLResponse DictionaryResponseWithP }; } } - } } diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs index 7ead796..e90af83 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/LinkTagHelperFixture.cs @@ -784,7 +784,6 @@ public void Process_AnchorLinkTagWithAnchor_AddsAnchorToHrefAttribute( } #endregion - [Theory] [AutoNSubstituteData] public void Process_RenderingChromesAreNotNull_ChromesAreOutput( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs index aaba593..dde4261 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/RichTextTagHelperFixture.cs @@ -708,7 +708,6 @@ public void Process_DivTagWithEditableFieldAndEditableSetToFalseAndTextAttribute #endregion - [Theory] [AutoNSubstituteData] public void Process_RenderingChromesAreNotNull_ChromesAreOutput( diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs index 75cbe61..c0e4619 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/TextFieldTagHelperFixture.cs @@ -189,7 +189,6 @@ public void Process_EditableFieldAndEditableSetToFalse_GeneratesCorrectOutput(Te tagHelperOutput.Content.GetContent().Should().Be(TestText); } - [Theory] [AutoNSubstituteData] public void Process_RenderingChromesAreNotNull_ChromesAreOutput( From 1af1068f9cf2ab45e4831b654392c8d448137b91 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Wed, 26 Mar 2025 14:33:02 +1100 Subject: [PATCH 26/38] Removed ID property from EdtiableField as didnt need to be added --- .../Response/Model/EditableField.cs | 7 ------- .../Serialization/Converter/FieldConverterTests.cs | 12 ++++++------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs index f6a74ad..ae428a6 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/EditableField.cs @@ -15,13 +15,6 @@ public class EditableField [JsonPropertyName("editable")] public string EditableMarkup { get; set; } = string.Empty; - /// - /// Gets or Sets the id of the Field. - /// - [DataMember(Name = "Id")] - [JsonPropertyName("Id")] - public string Id { get; set; } = string.Empty; - /// public EditableChrome? OpeningChrome { get; set; } diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs index 0efa66f..de84120 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Serialization/Converter/FieldConverterTests.cs @@ -17,12 +17,12 @@ public class FieldConverterTests public static TheoryData Fields => new() { - { new TextField("Test"), """{"editable":"","Id":"","value":"Test"}""" }, - { new RichTextField("Test Noencoding", false), """{"editable":"","Id":"","value":"Test Noencoding"}""" }, - { new RichTextField("Test%20Encoded"), """{"editable":"","Id":"","value":"Test Encoded"}""" }, - { new CheckboxField(true), """{"editable":"","Id":"","value":true}""" }, - { new CheckboxField(false), """{"editable":"","Id":""}""" }, - { new ImageField(new Image { Alt = "Alt Text", Border = 1, Class = "styleclass", HSpace = 1, Height = 100, Src = "https://image.com/test.jpg", Title = "Title", VSpace = 1, Width = 100 }), """{"editable":"","Id":"","value":{"src":"https://image.com/test.jpg","alt":"Alt Text","height":"100","width":"100","title":"Title","hSpace":"1","vSpace":"1","border":"1","class":"styleclass"}}""" } + { new TextField("Test"), """{"editable":"","value":"Test"}""" }, + { new RichTextField("Test Noencoding", false), """{"editable":"","value":"Test Noencoding"}""" }, + { new RichTextField("Test%20Encoded"), """{"editable":"","value":"Test Encoded"}""" }, + { new CheckboxField(true), """{"editable":"","value":true}""" }, + { new CheckboxField(false), """{"editable":""}""" }, + { new ImageField(new Image { Alt = "Alt Text", Border = 1, Class = "styleclass", HSpace = 1, Height = 100, Src = "https://image.com/test.jpg", Title = "Title", VSpace = 1, Width = 100 }), """{"editable":"","value":{"src":"https://image.com/test.jpg","alt":"Alt Text","height":"100","width":"100","title":"Title","hSpace":"1","vSpace":"1","border":"1","class":"styleclass"}}""" } }; [Fact] From af132612578a0d0a29dde176c324bd768c11cc7d Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Fri, 28 Mar 2025 14:25:19 +1100 Subject: [PATCH 27/38] Created Integration tests for PagesSetupController --- .../Controllers/PagesSetupController.cs | 3 +- .../Pages/PagesSetupRoutingFixture.cs | 131 ++++++++++++++++++ .../Fixtures/Pages/TestPagesProgram.cs | 39 ++++++ ...K.RenderingEngine.Integration.Tests.csproj | 2 + .../TestServerBuilder.cs | 1 + .../TestWebApplicationFactory.cs | 14 ++ .../TestConstants.cs | 10 ++ 7 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index e9e45d2..2de804f 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Sitecore.AspNetCore.SDK.Pages.Configuration; -using Sitecore.AspNetCore.SDK.Pages.Middleware; using Sitecore.AspNetCore.SDK.Pages.Models; using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.RenderingEngine.Configuration; @@ -147,4 +146,4 @@ private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) return false; } } -} +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs new file mode 100644 index 0000000..b6953f9 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs @@ -0,0 +1,131 @@ +using System.Net; +using FluentAssertions; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Pages; + +public class PagesSetupRoutingFixture(TestWebApplicationFactory factory) : IClassFixture> +{ + private readonly TestWebApplicationFactory factory = factory; + + [Fact] + public async Task ConfigRoute_MissingSecret_ReturnsBadRequest() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.ConfigRoute}?secret="; + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ConfigRoute_InvalidSecret_ReturnsBadRequest() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.ConfigRoute}?secret=invalid_secret_value"; + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ConfigRoute_InvalidRequestOrigin_ReturnsBadRequest() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.ConfigRoute}?secret={TestConstants.JssEditingSecret}"; + client.DefaultRequestHeaders.Add("Origin", "http://invalid_origin_domain.com"); + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task ConfigRoute_ValidCall_ReturnsCorrectObject() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.ConfigRoute}?secret={TestConstants.JssEditingSecret}"; + client.DefaultRequestHeaders.Add("Origin", "https://pages.sitecorecloud.io"); + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Content.Headers.ContentType.Should().NotBeNull(); + response.Content.Headers.ContentType?.MediaType.Should().Be("application/json"); + response.Headers.NonValidated["Content-Security-Policy"].Should().BeEquivalentTo("frame-ancestors 'self' https://pages.sitecorecloud.io"); + response.Headers.NonValidated["Access-Control-Allow-Origin"].Should().BeEquivalentTo("https://pages.sitecorecloud.io"); + response.Headers.NonValidated["Access-Control-Allow-Methods"].Should().BeEquivalentTo("GET, POST, OPTIONS, PUT, PATCH, DELETE"); + } + + [Fact] + public async Task RenderRoute_MissingSecret_ReturnsBadRequest() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.RenderRoute}?secret="; + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task RenderRoute_InvalidSecret_ReturnsBadRequest() + { + // Arrange + HttpClient client = factory.CreateClient(); + string url = $"{TestConstants.RenderRoute}?secret=invalid_secret_value"; + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + } + + [Fact] + public async Task RenderRoute_ValidCall_ReturnsCorrectResponse() + { + // Arrange + HttpClient client = factory.CreateClient(); + Guid itemId = Guid.NewGuid(); + string language = "en"; + string layoutKind = "final"; + string mode = "edit"; + string route = TestConstants.RenderRoute; // The controller needs to return a valid route in its RedirectResponse, so we're reusing the same route here instead of creating a fake one. + string site = "siteA"; + string version = "1"; + string tenantId = "tenant1234"; + string url = $"{TestConstants.RenderRoute}?secret={TestConstants.JssEditingSecret}&sc_itemid={itemId}&sc_lang={language}&sc_layoutKind={layoutKind}&mode={mode}&sc_site={site}&sc_version={version}&tenant_id={tenantId}&route={route}"; + + // Act + var response = await client.GetAsync(url); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Redirect); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs new file mode 100644 index 0000000..22a2a3b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs @@ -0,0 +1,39 @@ +using Sitecore.AspNetCore.SDK.GraphQL.Extensions; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Extensions; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +using Sitecore.AspNetCore.SDK.Pages.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.TestData; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRouting() + .AddMvc(); + +builder.Services.AddGraphQlClient(configuration => +{ + configuration.ContextId = TestConstants.ContextId; +}); + +builder.Services.AddSitecoreLayoutService() + .AddSitecorePagesHandler() + .AddGraphQlWithContextHandler("default", TestConstants.ContextId!, siteName: TestConstants.SiteName!) + .AsDefaultHandler(); + +builder.Services.AddSitecoreRenderingEngine(options => + { + options.AddDefaultPartialView("_ComponentNotFound"); + }) + .WithSitecorePages(TestConstants.ContextId, options => { options.EditingSecret = TestConstants.JssEditingSecret; }); + +WebApplication app = builder.Build(); +app.UseSitecorePages(new PagesOptions { ConfigEndpoint = TestConstants.ConfigRoute }); +app.UseRouting(); +app.Run(); + +/// +/// Partial class allowing this TestProgram to be created by a WebApplicationFactory for integration testing. +/// +public partial class TestPagesProgram +{ +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj index e710a16..9571e85 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.csproj @@ -9,7 +9,9 @@ + + diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs index 55c091e..517a74d 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestServerBuilder.cs @@ -5,6 +5,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests; +[Obsolete("This class is deprecated as this is based on the old .NET Core 3.0 approach app initialisation approach. Use `TestWebApplicationFactory` approach instead.")] public class TestServerBuilder { private readonly IWebHostBuilder _webHostBuilder = PrepareDefault(); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs new file mode 100644 index 0000000..13cb195 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc.Testing; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests +{ + public class TestWebApplicationFactory + : WebApplicationFactory + where T : class + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseContentRoot(Path.GetFullPath(Directory.GetCurrentDirectory())); + } + } +} \ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs index dd596c0..8abee9c 100644 --- a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs @@ -73,10 +73,20 @@ public static class TestConstants public const string JssEditingSecret = "mysecret"; + public const string ContextId = "a_context_id"; + + public const string SiteName = "siteA"; + public const string TestParamNameValue = "ParamName-Value"; + public const string ConfigRoute = "/api/editing/config"; + + public const string RenderRoute = "/api/editing/render"; + public const string HeadlessSxaLayoutId = "96e5f4ba-a2cf-4a4c-a4e7-64da88226362"; + public const string PagesSampleConfigRequest = "{}"; + #pragma warning disable SA1401 #pragma warning disable CA2211 public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; From 8109f8bc07afc7630df26535c56bfffb776f4136 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 1 Apr 2025 14:33:01 +1100 Subject: [PATCH 28/38] Added Integration test for succesful call to get editing layout with wrapped chromes --- .../GraphQL/GraphQLEditingServiceHandler.cs | 3 +- .../Controllers/PagesController.cs | 13 +++++ .../Fixtures/Pages/PagesEditingFixture.cs | 37 ++++++++++++ .../Fixtures/Pages/TestPagesProgram.cs | 5 ++ .../TestWebApplicationFactory.cs | 16 ++++- .../TestConstants.cs | 58 ++++++++++++++++++- 6 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs create mode 100644 tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 35982cb..734036d 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -1,6 +1,6 @@ -using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Sitecore.AspNetCore.SDK.GraphQL.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; @@ -13,6 +13,7 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.Pages.Services; +using System.Text.Json; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs new file mode 100644 index 0000000..1483c7e --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Controllers; + +public class PagesController : Controller +{ + public IActionResult Index() + { + var context = HttpContext.GetSitecoreRenderingContext(); + return View("~/Views/Shared/HeadlessSxaLayout.cshtml", context); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs new file mode 100644 index 0000000..b697b8b --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs @@ -0,0 +1,37 @@ +using System.Net; +using FluentAssertions; +using GraphQL; +using NSubstitute; +using Sitecore.AspNetCore.SDK.GraphQL.Request; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.TestData; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Pages; + +public class PagesEditingFixture(TestWebApplicationFactory factory) : IClassFixture> +{ + private readonly TestWebApplicationFactory factory = factory; + + [Fact] + public async Task EditingRequest_ValidRequest_ReturnsChromeDecoratedResponse() + { + // Arrange + factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.SimpleEditingLayoutQueryResponse); + factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.DictionaryResponseWithoutPaging); + + HttpClient client = factory.CreateClient(); + string url = $"/Pages/index?mode=edit&secret={TestConstants.JssEditingSecret}&sc_itemid={TestConstants.TestItemId}&sc_version=1&sc_layoutKind=final"; + + // Act + var response = await client.GetAsync(url); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrEmpty(); + responseBody.Should().Contain(""); + responseBody.Should().Contain(""); + } +} \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs index 22a2a3b..cf65cf0 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs @@ -29,6 +29,11 @@ WebApplication app = builder.Build(); app.UseSitecorePages(new PagesOptions { ConfigEndpoint = TestConstants.ConfigRoute }); app.UseRouting(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Pages}/{action=Index}"); + app.Run(); /// diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs index 13cb195..64f1474 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc.Testing; +using GraphQL.Client.Abstractions; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NSubstitute; namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests { @@ -6,9 +10,17 @@ public class TestWebApplicationFactory : WebApplicationFactory where T : class { + public IGraphQLClient MockGraphQLClient { get; set; } = Substitute.For(); + protected override void ConfigureWebHost(IWebHostBuilder builder) { - builder.UseContentRoot(Path.GetFullPath(Directory.GetCurrentDirectory())); + builder.UseContentRoot(Path.GetFullPath(Directory.GetCurrentDirectory())) + .ConfigureTestServices(services => + { + ServiceProvider serviceProvider = services.BuildServiceProvider(); + ServiceDescriptor descriptor = new(typeof(IGraphQLClient), MockGraphQLClient); + services.Replace(descriptor); + }); } } } \ No newline at end of file diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs index 8abee9c..da2f60f 100644 --- a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs @@ -1,5 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; +using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text.Json; namespace Sitecore.AspNetCore.SDK.TestData; @@ -87,6 +91,58 @@ public static class TestConstants public const string PagesSampleConfigRequest = "{}"; + public static GraphQLResponse SimpleEditingLayoutQueryResponse + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse(@"{ ""sitecore"" : { ""context"": { ""pageEditing"": true, ""site"": { ""name"": ""xmcloud-aspnet"" }, ""pageState"": ""edit"", ""editMode"": ""metadata"", ""clientData"": { ""hrz-canvas-state"": { ""itemId"": ""0f236cdb-b564-45df-89e3-d51c4635dc3d"", ""itemVersion"": 1, ""siteName"": ""xmcloud-aspnet"", ""language"": ""en"", ""deviceId"": ""fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3"", ""pageMode"": ""NORMAL"", ""variant"": null }, ""hrz-canvas-verification-token"": """" }, ""clientScripts"": [ ""https://xmc-sitecoresaaa790-xmcloudaspn93b0-testing80ef.sitecorecloud.io/sitecore modules/Shell/SXA/xa-inline-editor.js?v=40.2.135.4298"", ""https://feaasstatic.blob.core.windows.net/packages/page-extension/latest/page.js"", ""https://pages.sitecorecloud.io/horizon/canvas/horizon.canvas.js?v=3CD517344C4149E754F4C2E5BDED7C77"" ], ""language"": ""en"", ""itemPath"": ""/"" }, ""route"": { ""name"": ""Home"", ""displayName"": ""Home"", ""fields"": { ""Title"": { ""metadata"": { ""datasource"": { ""id"": ""{0F236CDB-B564-45DF-89E3-D51C4635DC3D}"", ""language"": ""en"", ""revision"": ""fa21d6a6-c5f0-43d6-87eb-029fae6564fc"", ""version"": 1 }, ""title"": ""Title"", ""fieldId"": ""{198CC08A-E984-4212-AD29-BFD75AF3AD6E}"", ""fieldType"": ""Single-Line Text"", ""rawValue"": ""Home"" }, ""value"": ""Home"" }, ""Content"": { ""metadata"": { ""datasource"": { ""id"": ""{0F236CDB-B564-45DF-89E3-D51C4635DC3D}"", ""language"": ""en"", ""revision"": ""fa21d6a6-c5f0-43d6-87eb-029fae6564fc"", ""version"": 1 }, ""title"": ""Content"", ""fieldId"": ""{869C97EE-296A-49B3-81B9-3FD9A131D104}"", ""fieldType"": ""Rich Text"", ""rawValue"": """" }, ""value"": """" }, ""NavigationFilter"": [], ""NavigationTitle"": { ""metadata"": { ""datasource"": { ""id"": ""{0F236CDB-B564-45DF-89E3-D51C4635DC3D}"", ""language"": ""en"", ""revision"": ""fa21d6a6-c5f0-43d6-87eb-029fae6564fc"", ""version"": 1 }, ""title"": ""Link caption in navigation"", ""fieldId"": ""{4E0720E9-9D50-4DDC-87CF-ECD65E8E94C8}"", ""fieldType"": ""Single-Line Text"", ""rawValue"": ""Home"" }, ""value"": ""Home"" }, ""NavigationClass"": null, ""Page Design"": null, ""SxaTags"": [] }, ""databaseName"": ""master"", ""deviceId"": ""fe5d7fdf-89c0-4d99-9aa3-b5fbd009c9f3"", ""itemId"": ""0f236cdb-b564-45df-89e3-d51c4635dc3d"", ""itemLanguage"": ""en"", ""itemVersion"": 1, ""layoutId"": ""96e5f4ba-a2cf-4a4c-a4e7-64da88226362"", ""templateId"": ""4ab470a8-5dee-4c43-b074-73ece681d7b3"", ""templateName"": ""Page"", ""placeholders"": { ""headless-main"": [ ] } } }}").RootElement + } + } + }; + } + } + + public static GraphQLResponse DictionaryResponseWithoutPaging + { + get + { + return new GraphQLResponse + { + Data = new EditingDictionaryResponse + { + Site = new Pages.Request.Handlers.GraphQL.Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + Results = new List + { + new SiteInfoDictionaryItem + { + Key = "key1", + Value = "value1" + } + }, + PageInfo = new PageInfo + { + HasNext = false, + EndCursor = string.Empty + } + } + } + } + } + }; + } + } + #pragma warning disable SA1401 #pragma warning disable CA2211 public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; From 0a08769f7679d4565dd52df50254eec7af92f9d1 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 1 Apr 2025 14:35:19 +1100 Subject: [PATCH 29/38] Added csproj missing from previous commit --- .../Sitecore.AspNetCore.SDK.TestData.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj b/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj index 65e5a16..4d724bf 100644 --- a/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/Sitecore.AspNetCore.SDK.TestData.csproj @@ -5,6 +5,7 @@ + From 17e0b093eba4ce222f7b441c19be0be46ff0d357 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 1 Apr 2025 15:18:24 +1100 Subject: [PATCH 30/38] Improved logic for when to enable editing of Fields in MetaData editing mode --- .../Handlers/GraphQL/GraphQLEditingServiceHandler.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 734036d..632a5d8 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -193,14 +193,12 @@ private static void AddPlaceholderOpeningChrome(string name, string id, Placehol private static void ProcessField(Dictionary updatedFields, KeyValuePair field) { - if (field.Value is JsonSerializedField serialisedField && field.Key != "CustomContent") + EditableField? editableField; + if (field.Value is JsonSerializedField serialisedField + && field.Key != "CustomContent" && + serialisedField.TryRead(out editableField) + && editableField != null) { - var editableField = serialisedField.Read>(); - if (editableField == null) - { - return; - } - object openingChromeContent = new { datasource = new From 21f0e7762cf71a5a2bbde543e1b22ac4e0d32460 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 7 Apr 2025 13:32:25 +1000 Subject: [PATCH 31/38] Added handling for /config route OPTIONS request --- .../Controllers/PagesSetupController.cs | 2 ++ .../Controllers/PagesSetupControllerFixture.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index 2de804f..6fd59a8 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -27,6 +27,7 @@ public class PagesSetupController(IOptions options, ILogger /// PagesConfigResponse object. [HttpGet] + [HttpOptions] public ActionResult Config() { if (IsValidPagesConfigRequest(Request)) @@ -99,6 +100,7 @@ private void SetConfigResponseHeaders(HttpResponse httpResponse) httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; + httpResponse.Headers.AccessControlAllowHeaders = "Authorization"; httpResponse.StatusCode = StatusCodes.Status200OK; httpResponse.ContentType = "application/json"; } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs index aed93f3..98665c0 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -140,6 +140,7 @@ public void ConfigRoute_ValidRequest_OkResponseReturned(IOptions p httpContext.Response.Headers.ContentSecurityPolicy.Should().Equal($"frame-ancestors 'self' {ValidOrigins} {ValidEditingOrigin}"); httpContext.Response.Headers.AccessControlAllowOrigin.Should().Equal(ValidEditingOrigin); httpContext.Response.Headers.AccessControlAllowMethods.Should().Equal("GET, POST, OPTIONS, PUT, PATCH, DELETE"); + httpContext.Response.Headers.AccessControlAllowHeaders.Should().Equal("Authorization"); httpContext.Response.StatusCode.Should().Be(StatusCodes.Status200OK); httpContext.Response.ContentType.Should().Be("application/json"); response.Result.Should().BeOfType(); From 4371c2457b50614fe0515c0a08043d0b00c7f3eb Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Mon, 7 Apr 2025 14:56:39 +1000 Subject: [PATCH 32/38] Fixed correct component naming for ViewComponents and PartialViews --- .../RenderingEngineOptionsExtensions.cs | 20 ++++++++++++------- .../Rendering/PartialViewComponentRenderer.cs | 6 ++++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs index bbe9951..947b868 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs @@ -52,7 +52,8 @@ public static RenderingEngineOptions AddPartialView( return AddPartialView( options, name => layoutComponentName.Equals(name, StringComparison.OrdinalIgnoreCase), - partialViewPath); + partialViewPath, + layoutComponentName); } /// @@ -61,17 +62,19 @@ public static RenderingEngineOptions AddPartialView( /// The to configure. /// The predicate to use when attempting to match a layout component. /// The path of the partial view. + /// The name of the component as defined on the Sitecore Rendering Item. /// The so that additional calls can be chained. public static RenderingEngineOptions AddPartialView( this RenderingEngineOptions options, Predicate match, - string partialViewPath) + string partialViewPath, + string sitecoreComponentName = "") { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(partialViewPath); - ComponentRendererDescriptor descriptor = PartialViewComponentRenderer.Describe(match, partialViewPath); + ComponentRendererDescriptor descriptor = PartialViewComponentRenderer.Describe(match, partialViewPath, sitecoreComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); @@ -151,7 +154,7 @@ public static RenderingEngineOptions AddViewComponent( ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(viewComponentName); - ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName); + ComponentRendererDescriptor descriptor = ViewComponentComponentRenderer.Describe(match, viewComponentName, viewComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); @@ -198,7 +201,8 @@ public static RenderingEngineOptions AddModelBoundView( return AddModelBoundView( options, match => match.Equals(layoutComponentName, StringComparison.OrdinalIgnoreCase), - viewName); + viewName, + layoutComponentName); } /// @@ -208,11 +212,13 @@ public static RenderingEngineOptions AddModelBoundView( /// The to configure. /// A predicate to use when attempting to match a layout component. /// The view name. + /// The Component name as defined on the Sitecore Rendering Item. /// The so that additional calls can be chained. public static RenderingEngineOptions AddModelBoundView( this RenderingEngineOptions options, Predicate match, - string viewName) + string viewName, + string sitecoreComponentName = "") { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(match); @@ -224,7 +230,7 @@ public static RenderingEngineOptions AddModelBoundView( sp, RenderingEngineConstants.SitecoreViewComponents.DefaultSitecoreViewComponentName, viewName), - viewName); + sitecoreComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs index 1fabce1..cb2afc5 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/PartialViewComponentRenderer.cs @@ -28,14 +28,16 @@ public PartialViewComponentRenderer(string locator) /// /// A predicate to use when attempting to match a layout component. /// The string to use when locating the Partial View. + /// The component name defined on the Sitecore Rendering Item. /// An instance of that describes the . - public static ComponentRendererDescriptor Describe(Predicate match, string locator) + public static ComponentRendererDescriptor Describe(Predicate match, string locator, string sitecoreComponentName = "") { ArgumentNullException.ThrowIfNull(match); ArgumentException.ThrowIfNullOrWhiteSpace(locator); return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp, locator)); + sp => ActivatorUtilities.CreateInstance(sp, locator), + sitecoreComponentName); } /// From 17e6f29447eaa48382eea9b707a8b0fd516c3659 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 22 Apr 2025 10:47:47 +1000 Subject: [PATCH 33/38] Aligned empty placeholder classname with jss --- .../TagHelpers/PlaceholderTagHelper.cs | 2 +- .../TagHelpers/PlaceholderTagHelperFixture.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs index a83d6cb..160c833 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/PlaceholderTagHelper.cs @@ -73,7 +73,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu bool emptyEdit = IsInEditingMode(renderingContext) && IsPlaceHolderEmpty(placeholderFeatures); if (emptyEdit) { - output.Content.AppendHtml("
"); + output.Content.AppendHtml("
"); } bool foundPlaceholderFeatures = false; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs index cd9724f..8afcc80 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs @@ -522,7 +522,7 @@ public async Task ProcessAsync_PlaceholderContainsUnknownPlaceholderFeature_IsIn await sut.ProcessAsync(tagHelperContext, tagHelperOutput); // Assert - tagHelperOutput.Content.GetContent().Should().Be("
"); + tagHelperOutput.Content.GetContent().Should().Be("
"); } [Theory] From 8fd259f14643409e6313fd66652ef90e22b23d64 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 22 Apr 2025 20:26:32 +1000 Subject: [PATCH 34/38] Fixed issue with rendering of empty nested placeholders due to incorrect use of placeholder key --- .../GraphQL/GraphQLEditingServiceHandler.cs | 7 +++---- .../Handlers/GraphQL/PlaceholderWorkItem.cs | 17 +++++------------ .../GraphQLEditingServiceHandlerFixture.cs | 2 +- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 632a5d8..534d064 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -111,7 +111,7 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde var output = current.Output; // Add opening chrome for placeholder - AddPlaceholderOpeningChrome(current.Name, current.Id, output); + AddPlaceholderOpeningChrome(current.PlaceholderKey, current.Id, output); // Process all features in this placeholder foreach (var feature in current.Features) @@ -147,12 +147,11 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde // Add a work item to process this placeholder workStack.Push(new PlaceholderWorkItem( - "container-{*}", + placeholderKey, component.Id, placeholderValue, processedPlaceholder, - component, - placeholderKey)); + component)); // Store the processed placeholder for later assignment component.Placeholders[placeholderKey] = processedPlaceholder; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs index 69f60d5..ee7883f 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs @@ -5,24 +5,22 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// /// Represents a work item with associated features and output, optionally linked to a parent component. /// -/// Specifies the name of the work item. +/// Specifies the name of the work item. /// Defines a unique identifier for the work item. /// Holds the features associated with the work item. /// Contains the output related to the work item. /// Links to a parent component if applicable. -/// Provides an optional key for placeholder identification. public class PlaceholderWorkItem( - string name, + string placeholderKey, string id, Placeholder features, Placeholder output, - Component? parentComponent = null, - string? placeholderKey = null) + Component? parentComponent = null) { /// - /// Gets the name of an entity. It is a read-only property initialized with the value of the 'name' variable. + /// Gets the PlaceholderKey of an entity. It is a read-only property initialized with the value of the 'PlaceholderKey' variable. /// - public string Name { get; } = name; + public string PlaceholderKey { get; } = placeholderKey; /// /// Gets the unique identifier as a read-only string property. It is initialized with the value of 'id'. @@ -43,9 +41,4 @@ public class PlaceholderWorkItem( /// Gets the parent component of the current component. It is a read-only property initialized with the provided parentComponent. /// public Component? ParentComponent { get; } = parentComponent; - - /// - /// Gets the read-only property that returns the value of the placeholderKey variable. It is used to access a specific key for placeholders. - /// - public string? PlaceholderKey { get; } = placeholderKey; } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 5505caa..9c8a92a 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -207,7 +207,7 @@ public async Task Request_ValidRequest_NestedPlaceholderChromesAreAdded(IGraphQL result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].Should().BeOfType(); result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["chrometype"].Should().Be("placeholder"); result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["kind"].Should().Be("open"); - result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["id"].Should().Be("container-{*}_component_1"); + result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][0].As().Attributes["id"].Should().Be("nested_placeholder_1_component_1"); result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].Should().BeOfType(); result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["chrometype"].Should().Be("placeholder"); result?.Content?.Sitecore?.Route?.Placeholders["placeholder_1"][2].As().Placeholders["nested_placeholder_1"][1].As().Attributes["kind"].Should().Be("close"); From 0bc23b7db4c31f9a1d3c513a6097e1dccb2ca21a Mon Sep 17 00:00:00 2001 From: Ivan Lieckens Date: Mon, 26 May 2025 14:11:24 +0200 Subject: [PATCH 35/38] Style refactor + QL vs Ql fixed (added QL to abbreviations) + Added additional options for valid methods and headers + Moved strings to constants + Adjusted multi return functions to single result + Resolved logging style warnings + Removed vars + Fixed variable naming + EE added to abbreviations --- Sitecore.AspNetCore.SDK.sln.DotSettings | 3 + .../Models/SitecoreGraphQLClientOptions.cs | 6 +- .../InvalidGraphQLConfigurationException.cs | 16 +-- .../GraphQlConfigurationExtensions.cs | 24 ++-- .../SitecoreLayoutClientBuilderExtensions.cs | 30 ++--- .../GraphQL/GraphQlLayoutServiceHandler.cs | 10 +- .../GraphQL/LayoutServiceGraphqlException.cs | 6 +- .../Configuration/PagesOptions.cs | 20 +++- .../Constants.cs | 51 +++++++++ .../Controllers/PagesSetupController.cs | 106 +++++++++--------- .../PagesAppConfigurationExtensions.cs | 2 +- .../Middleware/PagesRenderMiddleware.cs | 53 ++++----- .../Models/PagesConfigResponse.cs | 2 +- .../Models/PagesRenderArgs.cs | 8 +- .../GraphQL/EditingLayoutQueryResponse.cs | 3 +- .../GraphQL/GraphQLEditingServiceHandler.cs | 66 +++++------ .../Response/CanvasState.cs | 6 +- .../Services/DictionaryService.cs | 13 +-- .../TagHelpers/EditingScriptsTagHelper.cs | 20 ++-- .../MultisiteAppConfigurationExtensions.cs | 2 +- .../Services/GraphQlSiteCollectionService.cs | 6 +- .../TagHelpers/Fields/DateTagHelper.cs | 6 +- .../TagHelpers/Fields/ImageTagHelper.cs | 6 +- .../TagHelpers/Fields/LinkTagHelper.cs | 6 +- .../TagHelpers/Fields/NumberTagHelper.cs | 6 +- .../TagHelpers/Fields/RichTextTagHelper.cs | 6 +- .../TagHelpers/Fields/TextFieldTagHelper.cs | 6 +- ...coreRedirectsAppConfigurationExtensions.cs | 2 +- .../SitemapAppConfigurationExtensions.cs | 2 +- .../Services/GraphQlSiteInfoService.cs | 16 +-- .../GraphQlConfigurationExtensionsFixture.cs | 34 +++--- .../RequestsFixture.cs | 6 +- .../LayoutServiceGraphQlExceptionFixture.cs | 8 +- ...oreLayoutClientBuilderExtensionsFixture.cs | 8 +- .../GraphQlLayoutServiceHandlerFixture.cs | 10 +- .../HttpLayoutRequestHandlerFixture.cs | 2 +- .../PagesSetupControllerFixture.cs | 4 +- .../Request/Handlers/GraphQL/Constants.cs | 35 +++--- .../GraphQLEditingServiceHandlerFixture.cs | 2 +- .../Services/Constants.cs | 16 +-- .../Services/DictionaryServiceFixture.cs | 11 +- .../Controllers/PagesController.cs | 3 +- .../AdvanceLocalizationFixture.cs | 2 +- .../Localization/LocalizationFixture.cs | 2 +- ...lizationUsingAttributeMiddlewareFixture.cs | 2 +- .../Fixtures/Multisite/MultisiteFixture.cs | 6 +- .../Fixtures/Pages/PagesEditingFixture.cs | 12 +- .../Pages/PagesSetupRoutingFixture.cs | 30 ++--- .../Fixtures/Pages/TestPagesProgram.cs | 4 +- .../EdgeSitemapProxyFixture.cs | 6 +- ...tisiteAppConfigurationExtensionsFixture.cs | 2 +- .../Services/SiteResolverFixture.cs | 28 ++--- .../TestConstants.cs | 26 +++-- 53 files changed, 415 insertions(+), 353 deletions(-) create mode 100644 Sitecore.AspNetCore.SDK.sln.DotSettings diff --git a/Sitecore.AspNetCore.SDK.sln.DotSettings b/Sitecore.AspNetCore.SDK.sln.DotSettings new file mode 100644 index 0000000..f729d17 --- /dev/null +++ b/Sitecore.AspNetCore.SDK.sln.DotSettings @@ -0,0 +1,3 @@ + + EE + QL \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs index 23dc37c..4cfee2a 100644 --- a/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Client/Models/SitecoreGraphQLClientOptions.cs @@ -7,7 +7,7 @@ namespace Sitecore.AspNetCore.SDK.GraphQL.Client.Models; /// /// GraphQL Client options needed for Preview or Edge schemas. /// -public class SitecoreGraphQlClientOptions : GraphQLHttpClientOptions +public class SitecoreGraphQLClientOptions : GraphQLHttpClientOptions { /// /// ContextId query string key. @@ -35,12 +35,12 @@ public class SitecoreGraphQlClientOptions : GraphQLHttpClientOptions public string? ContextId { get; set; } /// - /// Gets or sets Default site name, used by middlewares which use GraphQl client. + /// Gets or sets Default site name, used by middlewares which use GraphQL client. /// public string? DefaultSiteName { get; set; } /// /// Gets or sets GraphQLJsonSerializer, which could be SystemTextJsonSerializer or NewtonsoftJsonSerializer, SystemTextJsonSerializer by default. /// - public IGraphQLWebsocketJsonSerializer GraphQlJsonSerializer { get; set; } = new SystemTextJsonSerializer(); + public IGraphQLWebsocketJsonSerializer GraphQLJsonSerializer { get; set; } = new SystemTextJsonSerializer(); } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs index 0cd0d78..2bd3c9f 100644 --- a/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Exceptions/InvalidGraphQLConfigurationException.cs @@ -1,32 +1,32 @@ namespace Sitecore.AspNetCore.SDK.GraphQL.Exceptions; /// -/// Details an exception that may occur during GraphQl configuration. +/// Details an exception that may occur during GraphQL configuration. /// -public class InvalidGraphQlConfigurationException : Exception +public class InvalidGraphQLConfigurationException : Exception { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public InvalidGraphQlConfigurationException() + public InvalidGraphQLConfigurationException() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The exception message. - public InvalidGraphQlConfigurationException(string message) + public InvalidGraphQLConfigurationException(string message) : base(message) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The exception message. /// The inner exception to be wrapped. - public InvalidGraphQlConfigurationException(string message, Exception innerException) + public InvalidGraphQLConfigurationException(string message, Exception innerException) : base(message, innerException) { } diff --git a/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs index 6adb9b4..434c71f 100644 --- a/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.GraphQL/Extensions/GraphQlConfigurationExtensions.cs @@ -10,37 +10,37 @@ namespace Sitecore.AspNetCore.SDK.GraphQL.Extensions; /// /// Sitemap configuration. /// -public static class GraphQlConfigurationExtensions +public static class GraphQLConfigurationExtensions { /// /// Configuration for GraphQLClient. /// /// The to add services to. - /// The configuration for GraphQL client. + /// The configuration for GraphQL client. /// The so that additional calls can be chained. - public static IServiceCollection AddGraphQlClient(this IServiceCollection services, Action configuration) + public static IServiceCollection AddGraphQLClient(this IServiceCollection services, Action configuration) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services.Configure(configuration); - SitecoreGraphQlClientOptions options = TryGetConfiguration(configuration); + SitecoreGraphQLClientOptions options = TryGetConfiguration(configuration); services.AddSingleton(_ => { if (!string.IsNullOrWhiteSpace(options.ContextId)) { options.EndPoint = options.EndPoint.AddQueryString( - SitecoreGraphQlClientOptions.ContextIdQueryStringKey, + SitecoreGraphQLClientOptions.ContextIdQueryStringKey, options.ContextId); } - GraphQLHttpClient graphQlHttpClient = new(options.EndPoint!, options.GraphQlJsonSerializer); + GraphQLHttpClient graphQlHttpClient = new(options.EndPoint!, options.GraphQLJsonSerializer); if (!string.IsNullOrWhiteSpace(options.ApiKey)) { - graphQlHttpClient.HttpClient.DefaultRequestHeaders.Add(SitecoreGraphQlClientOptions.ApiKeyHeaderName, options.ApiKey); + graphQlHttpClient.HttpClient.DefaultRequestHeaders.Add(SitecoreGraphQLClientOptions.ApiKeyHeaderName, options.ApiKey); } return graphQlHttpClient; @@ -49,23 +49,23 @@ public static IServiceCollection AddGraphQlClient(this IServiceCollection servic return services; } - private static SitecoreGraphQlClientOptions TryGetConfiguration(Action configuration) + private static SitecoreGraphQLClientOptions TryGetConfiguration(Action configuration) { - SitecoreGraphQlClientOptions options = new(); + SitecoreGraphQLClientOptions options = new(); configuration.Invoke(options); if (string.IsNullOrWhiteSpace(options.ApiKey) && string.IsNullOrWhiteSpace(options.ContextId)) { - throw new InvalidGraphQlConfigurationException(Resources.Exception_MissingApiKeyAndContextId); + throw new InvalidGraphQLConfigurationException(Resources.Exception_MissingApiKeyAndContextId); } if (options.EndPoint == null && !string.IsNullOrWhiteSpace(options.ContextId)) { - options.EndPoint = SitecoreGraphQlClientOptions.DefaultEdgeEndpoint; + options.EndPoint = SitecoreGraphQLClientOptions.DefaultEdgeEndpoint; } else if (options.EndPoint == null) { - throw new InvalidGraphQlConfigurationException(Resources.Exception_MissingEndpoint); + throw new InvalidGraphQLConfigurationException(Resources.Exception_MissingEndpoint); } return options; diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs index 4c5b2d1..1cdbddf 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Extensions/SitecoreLayoutClientBuilderExtensions.cs @@ -67,11 +67,11 @@ public static ILayoutRequestHandlerBuilder AddHandler( /// The being configured. /// The name used to identify the handler. /// The siteName used to identify the handler. - /// The apiKey to access graphQl endpoint. - /// GraphQl endpoint uri. - /// Default language for GraphQl requests. + /// The apiKey to access graphQL endpoint. + /// GraphQL endpoint uri. + /// Default language for GraphQL requests. /// The so that additional calls can be chained. - public static ILayoutRequestHandlerBuilder AddGraphQlHandler( + public static ILayoutRequestHandlerBuilder AddGraphQLHandler( this ISitecoreLayoutClientBuilder builder, string name, string siteName, @@ -101,11 +101,11 @@ public static ILayoutRequestHandlerBuilder AddGraph } }); return builder.AddHandler(name, sp - => ActivatorUtilities.CreateInstance( + => ActivatorUtilities.CreateInstance( sp, sp.GetRequiredKeyedService(name), sp.GetRequiredService(), - sp.GetRequiredService>())); + sp.GetRequiredService>())); } /// @@ -114,9 +114,9 @@ public static ILayoutRequestHandlerBuilder AddGraph /// The being configured. /// The name used to identify the handler. /// The siteName used to identify the handler. - /// Default language for GraphQl requests. + /// Default language for GraphQL requests. /// The so that additional calls can be chained. - public static ILayoutRequestHandlerBuilder AddGraphQlHandler( + public static ILayoutRequestHandlerBuilder AddGraphQLHandler( this ISitecoreLayoutClientBuilder builder, string name, string siteName, @@ -138,11 +138,11 @@ public static ILayoutRequestHandlerBuilder AddGraph } }); return builder.AddHandler(name, sp - => ActivatorUtilities.CreateInstance( + => ActivatorUtilities.CreateInstance( sp, sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService>())); + sp.GetRequiredService>())); } /// @@ -310,11 +310,11 @@ public static ILayoutRequestHandlerBuilder AddHttpHand /// The being configured. /// The name used to identify the handler. /// The context identifier to access graphQl endpoint. - /// GraphQl endpoint uri. + /// GraphQL endpoint uri. /// The siteName used to identify the handler. - /// Default language for GraphQl requests. + /// Default language for GraphQL requests. /// The so that additional calls can be chained. - public static ILayoutRequestHandlerBuilder AddGraphQlWithContextHandler( + public static ILayoutRequestHandlerBuilder AddGraphQLWithContextHandler( this ISitecoreLayoutClientBuilder builder, string name, string contextId, @@ -343,10 +343,10 @@ public static ILayoutRequestHandlerBuilder AddGraph } }); return builder.AddHandler(name, sp - => ActivatorUtilities.CreateInstance( + => ActivatorUtilities.CreateInstance( sp, sp.GetRequiredKeyedService(name), sp.GetRequiredService(), - sp.GetRequiredService>())); + sp.GetRequiredService>())); } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs index a3368c8..bd4e7ba 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/GraphQlLayoutServiceHandler.cs @@ -11,19 +11,19 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; /// /// -/// Initializes a new instance of the class. +/// Initializes a new instance of the class. /// /// The to use for logging. /// The graphQl client to handle response data. /// The serializer to handle response data. -public class GraphQlLayoutServiceHandler( +public class GraphQLLayoutServiceHandler( IGraphQLClient client, ISitecoreLayoutSerializer serializer, - ILogger logger) + ILogger logger) : ILayoutRequestHandler { private readonly ISitecoreLayoutSerializer _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); private readonly IGraphQLClient _client = client ?? throw new ArgumentNullException(nameof(client)); /// @@ -87,7 +87,7 @@ query LayoutQuery($path: String!, $language: String!, $site: String!) { if (response.Errors != null) { errors.AddRange( - response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQLException(e)))); } } diff --git a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs index 0f0a272..b4805f3 100644 --- a/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Request/Handlers/GraphQL/LayoutServiceGraphqlException.cs @@ -5,14 +5,14 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; /// /// -/// Initializes a new instance of the class. +/// Initializes a new instance of the class. /// /// GraphQL Error of a GraphQL Query. -public class LayoutServiceGraphQlException(GraphQLError error) +public class LayoutServiceGraphQLException(GraphQLError error) : SitecoreLayoutServiceClientException(error.Message) { /// /// Gets GraphQL Error of a GraphQL Query. /// - public GraphQLError GraphQlError { get; } = error ?? throw new ArgumentNullException(nameof(error)); + public GraphQLError GraphQLError { get; } = error ?? throw new ArgumentNullException(nameof(error)); } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs index 0108ae8..2d41bb5 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -13,27 +13,37 @@ public class PagesOptions /// /// Gets or sets the config endpoint for Pages MetaData mode. /// - public string? ConfigEndpoint { get; set; } = "/api/editing/config"; + public string ConfigEndpoint { get; set; } = "/api/editing/config"; /// /// Gets or sets the render endpoint for Pages MetaData mode. /// - public string? RenderEndpoint { get; set; } = "/api/editing/render"; + public string RenderEndpoint { get; set; } = "/api/editing/render"; /// /// Gets or sets the valid editing origin for all editing requests. /// - public string? ValidEditingOrigin { get; set; } = "https://pages.sitecorecloud.io"; + public string ValidEditingOrigin { get; set; } = "https://pages.sitecorecloud.io"; /// /// Gets or sets the valid origins for the head to run under. /// - public string? ValidOrigins { get; set; } = string.Empty; + public string ValidOrigins { get; set; } = string.Empty; + + /// + /// Gets or sets the valid HTTP methods. + /// + public string ValidMethods { get; set; } = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; + + /// + /// Gets or sets the valid headers. + /// + public string ValidHeaders { get; set; } = "Authorization"; /// /// Gets or sets the Editing Secret. /// - public string? EditingSecret { get; set; } = string.Empty; + public string EditingSecret { get; set; } = string.Empty; /// /// Gets or sets the number of entries per page in a dictionary. The default value is set to 1000. diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs index 41b68a3..e5f1499 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs @@ -26,5 +26,56 @@ public static class SitecoreTagHelpers /// public const string EditScriptsHtmlTag = "sc-editingscripts"; } + + /// + /// Const values for query string keys. + /// + public static class QueryStringKeys + { + /// + /// Mode query string key. + /// + public const string Mode = "mode"; + + /// + /// Item id query string key. + /// + public const string ItemId = "sc_itemid"; + + /// + /// Secret query string key. + /// + public const string Secret = "secret"; + + /// + /// Language query string key. + /// + public const string Language = "sc_lang"; + + /// + /// Layout kind query string key. + /// + public const string LayoutKind = "sc_layoutKind"; + + /// + /// Route query string key. + /// + public const string Route = "route"; + + /// + /// Site query string key. + /// + public const string Site = "sc_site"; + + /// + /// Version query string key. + /// + public const string Version = "sc_version"; + + /// + /// Tenant id query string key. + /// + public const string TenantId = "tenant_id"; + } } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index 6fd59a8..9b8dab5 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -15,12 +15,12 @@ namespace Sitecore.AspNetCore.SDK.Pages.Controllers /// /// The Sitecore Pages configuration options. /// The to use for logging. - /// The RenderingEngineOptions, used to retriece a list of all registered renderings for the applications. + /// The RenderingEngineOptions, used to retrieve a list of all registered renderings for the applications. public class PagesSetupController(IOptions options, ILogger logger, IOptions renderingEngineOptions) : ControllerBase { - private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); - private readonly RenderingEngineOptions renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); + private readonly PagesOptions _options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly RenderingEngineOptions _renderingEngineOptions = renderingEngineOptions != null ? renderingEngineOptions.Value : throw new ArgumentNullException(nameof(renderingEngineOptions)); /// /// The Config endpoint used to inform Pages how this editing host is configured and which components are implemented. @@ -32,7 +32,7 @@ public ActionResult Config() { if (IsValidPagesConfigRequest(Request)) { - logger.LogDebug(Resources.Debug_ProcessingValidPagesConfigRequest); + _logger.LogDebug("{Message}", Resources.Debug_ProcessingValidPagesConfigRequest); SetConfigResponseHeaders(Response); return Ok(BuildConfigResponseBody()); } @@ -47,43 +47,50 @@ public ActionResult Config() [HttpGet] public IActionResult Render() { + IActionResult result; if (IsValidPagesRenderRequest(Request)) { - logger.LogDebug(Resources.Debug_ProcessingValidPagesRenderRequest); + _logger.LogDebug("{Message}", Resources.Debug_ProcessingValidPagesRenderRequest); PagesRenderArgs args = ParseQueryStringArgs(Request); - return Redirect($"{args.Route}?mode={args.Mode}&sc_itemid={args.ItemId}&sc_version={args.Version}&sc_lang={args.Language}&sc_site={args.Site}&sc_layoutKind={args.LayoutKind}&secret={args.EditingSecret}&tenant_id={args.TenantId}&route={args.Route}"); + result = Redirect($"{args.Route}?{Constants.QueryStringKeys.Mode}={args.Mode}&{Constants.QueryStringKeys.ItemId}={args.ItemId}&{Constants.QueryStringKeys.Version}={args.Version}&{Constants.QueryStringKeys.Language}={args.Language}&{Constants.QueryStringKeys.Site}={args.Site}&{Constants.QueryStringKeys.LayoutKind}={args.LayoutKind}&{Constants.QueryStringKeys.Secret}={args.EditingSecret}&{Constants.QueryStringKeys.TenantId}={args.TenantId}&{Constants.QueryStringKeys.Route}={args.Route}"); + } + else + { + result = BadRequest(); } - return BadRequest(); + return result; } - private PagesRenderArgs ParseQueryStringArgs(HttpRequest request) + private static PagesRenderArgs ParseQueryStringArgs(HttpRequest request) { return new PagesRenderArgs { - ItemId = Guid.TryParse(request.Query["sc_itemid"].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, - EditingSecret = request.Query["secret"].FirstOrDefault() ?? string.Empty, - Language = request.Query["sc_lang"].FirstOrDefault() ?? string.Empty, - LayoutKind = request.Query["sc_layoutKind"].FirstOrDefault() ?? string.Empty, - Mode = request.Query["mode"].FirstOrDefault() ?? string.Empty, - Route = request.Query["route"].FirstOrDefault() ?? string.Empty, - Site = request.Query["sc_site"].FirstOrDefault() ?? string.Empty, - Version = int.TryParse(request.Query["sc_version"].FirstOrDefault(), out int version) ? version : 0, - TenantId = request.Query["tenant_id"].FirstOrDefault() ?? string.Empty, + ItemId = Guid.TryParse(request.Query[Constants.QueryStringKeys.ItemId].FirstOrDefault(), out Guid itemId) ? itemId : Guid.Empty, + EditingSecret = request.Query[Constants.QueryStringKeys.Secret].FirstOrDefault() ?? string.Empty, + Language = request.Query[Constants.QueryStringKeys.Language].FirstOrDefault() ?? string.Empty, + LayoutKind = request.Query[Constants.QueryStringKeys.LayoutKind].FirstOrDefault() ?? string.Empty, + Mode = request.Query[Constants.QueryStringKeys.Mode].FirstOrDefault() ?? string.Empty, + Route = request.Query[Constants.QueryStringKeys.Route].FirstOrDefault() ?? string.Empty, + Site = request.Query[Constants.QueryStringKeys.Site].FirstOrDefault() ?? string.Empty, + Version = int.TryParse(request.Query[Constants.QueryStringKeys.Version].FirstOrDefault(), out int version) ? version : 0, + TenantId = request.Query[Constants.QueryStringKeys.TenantId].FirstOrDefault() ?? string.Empty, }; } private bool IsValidPagesRenderRequest(HttpRequest httpRequest) { - ArgumentNullException.ThrowIfNull(httpRequest); - - if (!IsValidEditingSecret(httpRequest)) + bool result = false; + if (IsValidEditingSecret(httpRequest)) { - logger.LogError(Resources.Error_InvalidPagesEditingSecretValue); - return false; + result = true; + } + else + { + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingSecretValue); } - return true; + return result; } private PagesConfigResponse BuildConfigResponseBody() @@ -91,61 +98,58 @@ private PagesConfigResponse BuildConfigResponseBody() return new PagesConfigResponse { EditMode = "metadata", - Components = renderingEngineOptions.RendererRegistry.Where(x => x.Value.ComponentName != string.Empty).Select(x => x.Value.ComponentName).ToList() + Components = _renderingEngineOptions.RendererRegistry.Where(x => x.Value.ComponentName != string.Empty).Select(x => x.Value.ComponentName).ToList() }; } private void SetConfigResponseHeaders(HttpResponse httpResponse) { - httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {options.ValidOrigins} {options.ValidEditingOrigin}"; - httpResponse.Headers.AccessControlAllowOrigin = options.ValidEditingOrigin; - httpResponse.Headers.AccessControlAllowMethods = "GET, POST, OPTIONS, PUT, PATCH, DELETE"; - httpResponse.Headers.AccessControlAllowHeaders = "Authorization"; + httpResponse.Headers.ContentSecurityPolicy = $"frame-ancestors 'self' {_options.ValidOrigins} {_options.ValidEditingOrigin}"; + httpResponse.Headers.AccessControlAllowOrigin = _options.ValidEditingOrigin; + httpResponse.Headers.AccessControlAllowMethods = _options.ValidMethods; + httpResponse.Headers.AccessControlAllowHeaders = _options.ValidHeaders; httpResponse.StatusCode = StatusCodes.Status200OK; httpResponse.ContentType = "application/json"; } private bool IsValidPagesConfigRequest(HttpRequest httpRequest) { - ArgumentNullException.ThrowIfNull(httpRequest); - - if (!IsValidEditingSecret(httpRequest)) - { - logger.LogError(Resources.Error_InvalidPagesEditingSecretValue); - return false; - } - - if (!RequestHasValidEditingOrigin(httpRequest)) - { - logger.LogError(Resources.Error_InvalidPagesEditingOrigin); - return false; - } - - return true; + return IsValidEditingSecret(httpRequest) && RequestHasValidEditingOrigin(httpRequest); } private bool IsValidEditingSecret(HttpRequest httpRequest) { - if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + bool result = false; + if (httpRequest.Query.TryGetValue(Constants.QueryStringKeys.Secret, out StringValues editingSecretValues)) { string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; - if (editingSecret == options.EditingSecret) + if (editingSecret == _options.EditingSecret) { - return true; + result = true; } } - return false; + if (!result) + { + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingOrigin); + } + + return result; } private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) { - if (httpRequest.Headers.Origin == options.ValidEditingOrigin) + bool result = false; + if (httpRequest.Headers.Origin == _options.ValidEditingOrigin) + { + result = true; + } + else { - return true; + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingOrigin); } - return false; + return result; } } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index 693bba1..ca820c4 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -26,7 +26,7 @@ public static class PagesAppConfigurationExtensions /// Registers the Sitecore Experience Editor middleware into the . /// /// The instance of the to extend. - /// The Pages options used to configure Pages MetaData edting. + /// The Pages options used to configure Pages MetaData editing. /// The so that additional calls can be chained. public static IApplicationBuilder UseSitecorePages(this WebApplication app, PagesOptions options) { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs index 9718c88..48330bb 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Sitecore.AspNetCore.SDK.LayoutService.Client.Interfaces; @@ -26,14 +25,13 @@ namespace Sitecore.AspNetCore.SDK.Pages.Middleware; /// The Sitecore Pages configuration options. /// The to map the HttpRequest to a Layout Service request. /// The layout service client. -/// The to use for logging. -public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService, ILogger logger) +public class PagesRenderMiddleware(RequestDelegate next, IOptions options, ISitecoreLayoutRequestMapper requestMapper, ISitecoreLayoutClient layoutService) { - private readonly RequestDelegate next = next ?? throw new ArgumentNullException(nameof(next)); - private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); - private readonly ISitecoreLayoutRequestMapper _requestMapper = requestMapper ?? throw new ArgumentNullException(nameof(requestMapper)); - private readonly ISitecoreLayoutClient layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly RequestDelegate _next = next ?? throw new ArgumentNullException(nameof(next)); + + private readonly PagesOptions _options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + + private readonly ISitecoreLayoutClient _layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); /// /// The middleware Invoke method. @@ -79,47 +77,42 @@ public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewCompo httpContext.Items.Add(nameof(PagesRenderMiddleware), null); } - await next(httpContext).ConfigureAwait(false); + await _next(httpContext).ConfigureAwait(false); } - private bool IsValidEditingRequest(HttpContext context) + private static bool IsInEditMode(HttpContext context) { - if (context.Request.Path == options.RenderEndpoint) - { - return false; - } - - if (!context.Request.Query.TryGetValue("mode", out var mode) || mode != "edit") - { - return false; - } - - if (!IsValidEditingSecret(context.Request)) - { - return false; - } + return context.Request.Query.TryGetValue(Constants.QueryStringKeys.Mode, out StringValues mode) + && mode == "edit"; + } - return true; + private bool IsValidEditingRequest(HttpContext context) + { + return + context.Request.Path != _options.RenderEndpoint + && IsInEditMode(context) + && IsValidEditingSecret(context.Request); } private bool IsValidEditingSecret(HttpRequest httpRequest) { - if (httpRequest.Query.TryGetValue("secret", out StringValues editingSecretValues)) + bool result = false; + if (httpRequest.Query.TryGetValue(Constants.QueryStringKeys.Secret, out StringValues editingSecretValues)) { string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; - if (editingSecret == options.EditingSecret) + if (editingSecret == _options.EditingSecret) { - return true; + result = true; } } - return false; + return result; } private async Task GetSitecoreLayoutResponse(HttpContext httpContext) { SitecoreLayoutRequest request = requestMapper.Map(httpContext.Request); ArgumentNullException.ThrowIfNull(request); - return await layoutService.Request(request, Constants.LayoutClients.Pages).ConfigureAwait(false); + return await _layoutService.Request(request, Constants.LayoutClients.Pages).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs index b192052..b44b9a1 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesConfigResponse.cs @@ -8,7 +8,7 @@ public class PagesConfigResponse /// /// Gets or sets the edit mode for the Pages Config endpoint. /// - public List Components { get; set; } = new(); + public List Components { get; set; } = []; /// /// Gets or sets the edit mode for the Pages Config endpoint. diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs index 683f009..afce819 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs @@ -1,6 +1,4 @@ -using GraphQL; - -namespace Sitecore.AspNetCore.SDK.Pages.Models +namespace Sitecore.AspNetCore.SDK.Pages.Models { /// /// The model used to store the args passed in to the Render route when using Pages MetaData Editing mode. @@ -13,7 +11,7 @@ public class PagesRenderArgs public Guid ItemId { get; set; } /// - /// Gets or sets the lanugage of the item being edited. + /// Gets or sets the language of the item being edited. /// public string Language { get; set; } = string.Empty; @@ -33,7 +31,7 @@ public class PagesRenderArgs public string LayoutKind { get; set; } = string.Empty; /// - /// Gets or sets the mode that the redering is running. + /// Gets or sets the mode that the rendering is running. /// public string Mode { get; set; } = string.Empty; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs index c46aec4..cd5f886 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -1,5 +1,4 @@ -using System.Text.Json.Serialization; -using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 534d064..332334b 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -1,6 +1,6 @@ +using System.Text.Json; using GraphQL; using GraphQL.Client.Abstractions; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Sitecore.AspNetCore.SDK.GraphQL.Request; using Sitecore.AspNetCore.SDK.LayoutService.Client.Exceptions; @@ -11,9 +11,9 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; +using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.Pages.Services; -using System.Text.Json; namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; @@ -25,16 +25,16 @@ namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; /// The to use for logging. /// DictionaryService used to return all dictionary items for a Sitecore site. /// The serializer to handle response data. -public partial class GraphQLEditingServiceHandler(IGraphQLClient client, +public class GraphQLEditingServiceHandler(IGraphQLClient client, ISitecoreLayoutSerializer serializer, ILogger logger, IDictionaryService dictionaryService) : ILayoutRequestHandler { - private readonly IGraphQLClient client = client ?? throw new ArgumentNullException(nameof(client)); - private readonly ISitecoreLayoutSerializer serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - private readonly ILogger logger = logger ?? throw new ArgumentNullException(nameof(logger)); - private readonly IDictionaryService dictionaryService = dictionaryService ?? throw new ArgumentNullException(nameof(dictionaryService)); + private readonly IGraphQLClient _client = client ?? throw new ArgumentNullException(nameof(client)); + private readonly ISitecoreLayoutSerializer _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IDictionaryService _dictionaryService = dictionaryService ?? throw new ArgumentNullException(nameof(dictionaryService)); /// public async Task Request(SitecoreLayoutRequest request, string handlerName) @@ -70,14 +70,16 @@ public async Task Request(SitecoreLayoutRequest request, private static bool IsEditingRequest(SitecoreLayoutRequest request) { - if (!request.ContainsKey("sc_request_headers_key") || - request["sc_request_headers_key"] is not Dictionary headers || - !headers.ContainsKey("mode")) + bool result = false; + if (request.TryGetHeadersCollection(out Dictionary? headers)) { - return false; + if (headers != null && headers.TryGetValue(Constants.QueryStringKeys.Mode, out string[]? value)) + { + result = value.Contains("edit"); + } } - return headers["mode"].Contains("edit"); + return result; } private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? content) @@ -87,7 +89,7 @@ private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? conte return; } - foreach (var placeholder in content.Sitecore.Route.Placeholders) + foreach (KeyValuePair placeholder in content.Sitecore.Route.Placeholders) { string name = placeholder.Key; Placeholder placeholderFeatures = placeholder.Value; @@ -98,23 +100,23 @@ private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? conte private static Placeholder ProcessPlaceholder(string name, string id, Placeholder placeholderFeatures) { - Placeholder result = new(); + Placeholder result = []; // Create a separate class outside of this method for the work item // to avoid nested class compilation issues - var workStack = new Stack(); + Stack workStack = new(); workStack.Push(new PlaceholderWorkItem(name, id, placeholderFeatures, result)); while (workStack.Count > 0) { - var current = workStack.Pop(); - var output = current.Output; + PlaceholderWorkItem current = workStack.Pop(); + Placeholder output = current.Output; // Add opening chrome for placeholder AddPlaceholderOpeningChrome(current.PlaceholderKey, current.Id, output); // Process all features in this placeholder - foreach (var feature in current.Features) + foreach (IPlaceholderFeature feature in current.Features) { if (feature is Component component) { @@ -123,7 +125,7 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde // Process fields Dictionary updatedFields = new(); - foreach (var field in component.Fields) + foreach (KeyValuePair field in component.Fields) { ProcessField(updatedFields, field); } @@ -137,13 +139,13 @@ private static Placeholder ProcessPlaceholder(string name, string id, Placeholde if (component.Placeholders.Count > 0) { // For each placeholder in the component, add it to the work stack - foreach (var placeholder in component.Placeholders.ToList()) + foreach (KeyValuePair placeholder in component.Placeholders.ToList()) { string placeholderKey = placeholder.Key; Placeholder placeholderValue = placeholder.Value; // Create a new placeholder to hold the processed content - Placeholder processedPlaceholder = new(); + Placeholder processedPlaceholder = []; // Add a work item to process this placeholder workStack.Push(new PlaceholderWorkItem( @@ -216,8 +218,8 @@ private static void ProcessField(Dictionary updatedFields, editableField.OpeningChrome = GenerateEditableChrome("field", "open", string.Empty, JsonSerializer.Serialize(openingChromeContent)); editableField.ClosingChrome = GenerateEditableChrome("field", "close", string.Empty, string.Empty); - var editableFieldWithChromesJson = JsonSerializer.SerializeToDocument(editableField); - var updatedJsonSerialisedField = new JsonSerializedField(editableFieldWithChromesJson); + JsonDocument editableFieldWithChromesJson = JsonSerializer.SerializeToDocument(editableField); + JsonSerializedField updatedJsonSerialisedField = new(editableFieldWithChromesJson); updatedFields.Add(field.Key, updatedJsonSerialisedField); } @@ -229,7 +231,7 @@ private static void ProcessField(Dictionary updatedFields, private static EditableChrome GenerateEditableChrome(string chrometype, string kind, string id, string content) { - EditableChrome editableChrome = new EditableChrome + EditableChrome editableChrome = new() { Attributes = { @@ -297,18 +299,18 @@ query EditingQuery( private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) { - GraphQLResponse response = await client.SendQueryAsync(BuildEditingLayoutRequest(request, requestLanguage)).ConfigureAwait(false); + GraphQLResponse response = await _client.SendQueryAsync(BuildEditingLayoutRequest(request, requestLanguage)).ConfigureAwait(false); if (response?.Data == null) { throw new Exception(Resources.Exception_UableToProcessEditingResponse); } - response.Data.Site.SiteInfo.Dictionary.Results = await dictionaryService.GetSiteDictionary(request.SiteName() ?? string.Empty, requestLanguage, client); + response.Data.Site.SiteInfo.Dictionary.Results = await _dictionaryService.GetSiteDictionary(request.SiteName() ?? string.Empty, requestLanguage, _client); - if (logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { - logger.LogDebug(Resources.Debug_LayoutServiceGraphQLResponse, response.Data.Item); + _logger.LogDebug(Resources.Debug_LayoutServiceGraphQLResponse, response.Data.Item); } SitecoreLayoutResponseContent? content = null; @@ -319,20 +321,20 @@ query EditingQuery( } else { - content = serializer.Deserialize(json); + content = _serializer.Deserialize(json); GenerateMetaDataChromes(content); - if (logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Debug)) { object? formattedDeserializeObject = JsonSerializer.Deserialize(json); - logger.LogDebug(Resources.Debug_LayoutServiceResponseJSON, formattedDeserializeObject); + _logger.LogDebug(Resources.Debug_LayoutServiceResponseJSON, formattedDeserializeObject); } } if (response.Errors != null) { - errors.AddRange(response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQlException(e)))); + errors.AddRange(response.Errors.Select(e => new SitecoreLayoutServiceClientException(new LayoutServiceGraphQLException(e)))); } return content; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs index 0e07bcf..e46d915 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Response/CanvasState.cs @@ -8,7 +8,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.Response; public class CanvasState { /// - /// Gets or sets the Id of the item being edited. + /// Gets or sets the id of the item being edited. /// [JsonPropertyName("itemId")] public string? ItemId { get; set; } @@ -32,7 +32,7 @@ public class CanvasState public string? Language { get; set; } /// - /// Gets or sets the Id of the Device being edited. + /// Gets or sets the id of the device being edited. /// [JsonPropertyName("deviceId")] public string? DeviceId { get; set; } @@ -44,7 +44,7 @@ public class CanvasState public string? PageMode { get; set; } /// - /// Gets or sets the current id of the Varient being edited. + /// Gets or sets the current id of the variant being edited. /// [JsonPropertyName("variant")] public string? Variant { get; set; } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs index 9e8e1e8..76938cf 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs @@ -11,7 +11,7 @@ namespace Sitecore.AspNetCore.SDK.Pages.Services; /// public class DictionaryService(IOptions options) : IDictionaryService { - private readonly PagesOptions options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); + private readonly PagesOptions _options = options != null ? options.Value : throw new ArgumentNullException(nameof(options)); /// /// Retrieves a list of site information dictionary items based on the specified site and language. @@ -27,13 +27,12 @@ public async Task> GetSiteDictionary(string siteNam throw new ArgumentNullException(nameof(siteName)); } - List dictionary = new(); + List dictionary = []; GraphQLResponse dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, string.Empty).ConfigureAwait(false); - while (dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.HasNext ?? false - && (dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty) != string.Empty) + while (dictionaryPageResponse.Data.Site?.SiteInfo.Dictionary.PageInfo?.HasNext ?? false) { - dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.PageInfo?.EndCursor ?? string.Empty).ConfigureAwait(false); + dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, dictionaryPageResponse.Data.Site?.SiteInfo.Dictionary.PageInfo?.EndCursor ?? string.Empty).ConfigureAwait(false); } return dictionary; @@ -43,7 +42,7 @@ private async Task> GetSinglePageOfDi { GraphQLRequest dictionaryPageRequest = BuildEditingDictionaryRequest(siteName, requestLanguage, endCursor); GraphQLResponse dictionaryPageResponse = await client.SendQueryAsync(dictionaryPageRequest).ConfigureAwait(false); - dictionary.AddRange(dictionaryPageResponse.Data?.Site?.SiteInfo?.Dictionary?.Results ?? []); + dictionary.AddRange(dictionaryPageResponse.Data.Site?.SiteInfo.Dictionary.Results ?? []); return dictionaryPageResponse; } @@ -79,7 +78,7 @@ query DictionaryQuery( { language = requestLanguage, siteName = siteName, - pageSize = options.DictionaryPageSize, + pageSize = _options.DictionaryPageSize, after = endCursor } }; diff --git a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs index 22e2bba..bb22efd 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -31,9 +31,9 @@ public override void Process(TagHelperContext context, TagHelperOutput output) output.TagName = string.Empty; string html = string.Empty; - if (renderingContext?.Response?.Content?.Sitecore?.Context?.IsEditing ?? false) + if (renderingContext.Response?.Content?.Sitecore?.Context?.IsEditing ?? false) { - EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext?.Response?.Content.ContextRawData ?? string.Empty); + EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext.Response?.Content.ContextRawData ?? string.Empty); if (editingContext == null) { throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperUnableToProcessContextRawData); @@ -47,18 +47,18 @@ public override void Process(TagHelperContext context, TagHelperOutput output) html += $@" "; - html += $""; + html += $""; } output.Content.SetHtmlContent(html); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs index e77e4d3..50f02d4 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/MultisiteAppConfigurationExtensions.cs @@ -47,7 +47,7 @@ public static IServiceCollection AddMultisite(this IServiceCollection services, services.Configure(configuration); } - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); return services; diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs index 9f23b1e..ca64bcd 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Services/GraphQlSiteCollectionService.cs @@ -6,11 +6,11 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.Services; /// -/// GraphQl Site Collection Service. +/// GraphQL Site Collection Service. /// -/// The GraphQl Client. +/// The GraphQL Client. /// The Logger. -internal class GraphQlSiteCollectionService(IGraphQLClient graphQlClient, ILogger logger) +internal class GraphQLSiteCollectionService(IGraphQLClient graphQlClient, ILogger logger) : ISiteCollectionService { private const string SiteInfoCollectionQuery = """ diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs index 696c7ce..a0ef61a 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/DateTagHelper.cs @@ -16,7 +16,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.DateTagHelperAttribute)] public class DateTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -73,14 +73,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (field.OpeningChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.OpeningChrome)); } output.Content.AppendHtml(html); if (field.ClosingChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.ClosingChrome)); } } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs index 7cec677..84adca1 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/ImageTagHelper.cs @@ -29,7 +29,7 @@ public class ImageTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper private const string VSpaceAttribute = "vspace"; private const string TitleAttribute = "title"; private const string BorderAttribute = "border"; - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -84,14 +84,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output) { if (field.OpeningChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.OpeningChrome)); } output.Content.AppendHtml(GenerateImage(field, output)); if (field.ClosingChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.ClosingChrome)); } } else diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs index c41d017..b5dfef4 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/LinkTagHelper.cs @@ -26,7 +26,7 @@ public class LinkTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper private const string RelAttribute = "rel"; private const string BlankValue = "_blank"; private const string AnchorValue = "#"; - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer; + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer; /// /// Gets or sets the model value. @@ -200,7 +200,7 @@ private void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) { if (field.OpeningChrome != null && field.ClosingChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.OpeningChrome)); if (field.Value.Href == string.Empty) { @@ -211,7 +211,7 @@ private void RenderEditableMarkup(TagHelperOutput output, HyperLinkField field) output.Content.AppendHtml(GenerateLink(field.Value, output)); } - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.ClosingChrome)); } else { diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs index 513eabf..f535d1b 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs @@ -15,7 +15,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.NumberTagHelperAttribute)] public class NumberTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -75,14 +75,14 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (field.OpeningChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.OpeningChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.OpeningChrome)); } output.Content.AppendHtml(value); if (field.ClosingChrome != null) { - output.Content.AppendHtml(chromeRenderer.Render(field.ClosingChrome)); + output.Content.AppendHtml(_chromeRenderer.Render(field.ClosingChrome)); } } } \ No newline at end of file diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs index a4006ca..da91a54 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/RichTextTagHelper.cs @@ -16,7 +16,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.TextTagHelperAttribute)] public class RichTextTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -54,7 +54,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) string html = string.Empty; if (Editable && richTextField.OpeningChrome != null) { - html += chromeRenderer.Render(richTextField.OpeningChrome); + html += _chromeRenderer.Render(richTextField.OpeningChrome); html += "
"; } @@ -66,7 +66,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (Editable && richTextField.ClosingChrome != null) { html += "
"; - html += chromeRenderer.Render(richTextField.ClosingChrome); + html += _chromeRenderer.Render(richTextField.ClosingChrome); } output.Content.SetHtmlContent(new HtmlString(html)); diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs index 1f04df7..e803b53 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/TextFieldTagHelper.cs @@ -13,7 +13,7 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.TagHelpers.Fields; [HtmlTargetElement("*", Attributes = RenderingEngineConstants.SitecoreTagHelpers.AspForTagHelperAttribute)] public partial class TextFieldTagHelper(IEditableChromeRenderer chromeRenderer) : TagHelper { - private readonly IEditableChromeRenderer chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); + private readonly IEditableChromeRenderer _chromeRenderer = chromeRenderer ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -50,7 +50,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) bool isHtml = false; if (Editable && field.OpeningChrome != null) { - html += chromeRenderer.Render(field.OpeningChrome); + html += _chromeRenderer.Render(field.OpeningChrome); html += "
"; isHtml = true; } @@ -68,7 +68,7 @@ public override void Process(TagHelperContext context, TagHelperOutput output) if (Editable && field.ClosingChrome != null) { html += "
"; - html += chromeRenderer.Render(field.ClosingChrome); + html += _chromeRenderer.Render(field.ClosingChrome); } if (isHtml) diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs index 7498a28..02dcb33 100644 --- a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitecoreRedirectsAppConfigurationExtensions.cs @@ -40,7 +40,7 @@ public static IServiceCollection AddSitecoreRedirects(this IServiceCollection se { ArgumentNullException.ThrowIfNull(services); - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs index bb3f221..98c33ca 100644 --- a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Extensions/SitemapAppConfigurationExtensions.cs @@ -77,7 +77,7 @@ public static IServiceCollection AddEdgeSitemap(this IServiceCollection services services.AddProxy(); - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs index e8ab873..50400bd 100644 --- a/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs +++ b/src/Sitecore.AspNetCore.SDK.SearchOptimization/Services/GraphQlSiteInfoService.cs @@ -9,15 +9,15 @@ namespace Sitecore.AspNetCore.SDK.SearchOptimization.Services; /// -/// Implements the Sitemap and Redirects services by GraphQl data retrieval from Edge or Preview Delivery API. +/// Implements the Sitemap and Redirects services by GraphQL data retrieval from Edge or Preview Delivery API. /// -/// Options to configure the GraphQl Client. -/// GraphQl Client. +/// Options to configure the GraphQL Client. +/// GraphQL Client. /// Logger service. -internal class GraphQlSiteInfoService( - IOptions options, +internal class GraphQLSiteInfoService( + IOptions options, IGraphQLClient graphQlClient, - ILogger logger) + ILogger logger) : ISitemapService, IRedirectsService { private const string SiteInfoQuerySitemap = """ @@ -46,7 +46,7 @@ query SiteInfoQuery($site: String!) { } """; - private readonly SitecoreGraphQlClientOptions _options = options.Value; + private readonly SitecoreGraphQLClientOptions _options = options.Value; /// public async Task GetSitemapUrl(string requestedUrl, string? siteName) @@ -128,7 +128,7 @@ private static void EnsureSiteName(string? siteName) { if (string.IsNullOrWhiteSpace(siteName)) { - throw new InvalidGraphQlConfigurationException("Empty DefaultSiteName, provided in GraphQLClientOptions."); + throw new InvalidGraphQLConfigurationException("Empty DefaultSiteName, provided in GraphQLClientOptions."); } } } \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs index ad30c04..384b284 100644 --- a/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Extensions/GraphQlConfigurationExtensionsFixture.cs @@ -13,70 +13,70 @@ namespace Sitecore.AspNetCore.SDK.GraphQL.Tests.Extensions; -public class GraphQlConfigurationExtensionsFixture +public class GraphQLConfigurationExtensionsFixture { [Theory] [AutoNSubstituteData] - public void AddGraphQlClient_NullProperties_ThrowsExceptions(IServiceCollection serviceCollection) + public void AddGraphQLClient_NullProperties_ThrowsExceptions(IServiceCollection serviceCollection) { Func servicesNull = - () => GraphQlConfigurationExtensions.AddGraphQlClient(null!, null!); + () => GraphQLConfigurationExtensions.AddGraphQLClient(null!, null!); Func configNull = - () => serviceCollection.AddGraphQlClient(null!); + () => serviceCollection.AddGraphQLClient(null!); servicesNull.Should().Throw().WithParameterName("services"); configNull.Should().Throw().WithParameterName("configuration"); } [Fact] - public void AddGraphQlClient_EmptyApiKey_InConfiguration_ThrowsExceptions() + public void AddGraphQLClient_EmptyApiKey_InConfiguration_ThrowsExceptions() { Func act = - () => Substitute.For().AddGraphQlClient(_ => { }); - act.Should().Throw() + () => Substitute.For().AddGraphQLClient(_ => { }); + act.Should().Throw() .WithMessage(Resources.Exception_MissingApiKeyAndContextId); } [Theory] [AutoData] - public void AddGraphQlClient_EmptyEndpoint_WithApiKey_ThrowsExceptions(string apiKey) + public void AddGraphQLClient_EmptyEndpoint_WithApiKey_ThrowsExceptions(string apiKey) { Func act = - () => Substitute.For().AddGraphQlClient(options => + () => Substitute.For().AddGraphQLClient(options => { options.ApiKey = apiKey; }); - act.Should().Throw() + act.Should().Throw() .WithMessage(Resources.Exception_MissingEndpoint); } [Theory] [AutoData] - public void AddGraphQlClient_EmptyEndpointUri_WithContextId_UsesDefault(string contextId) + public void AddGraphQLClient_EmptyEndpointUri_WithContextId_UsesDefault(string contextId) { // Arrange ServiceCollection serviceCollection = []; // Act - serviceCollection.AddGraphQlClient(configuration => + serviceCollection.AddGraphQLClient(configuration => { configuration.ContextId = contextId; }); GraphQLHttpClient? graphQlClient = serviceCollection.BuildServiceProvider().GetService() as GraphQLHttpClient; // Assert - graphQlClient!.Options.EndPoint!.OriginalString.Should().Contain(SitecoreGraphQlClientOptions.DefaultEdgeEndpoint.OriginalString); + graphQlClient!.Options.EndPoint!.OriginalString.Should().Contain(SitecoreGraphQLClientOptions.DefaultEdgeEndpoint.OriginalString); } [Theory] [AutoData] - public void AddGraphQlClient_AddConfiguredGraphQlClient_To_ServiceCollection(string apiKey, Uri endpointUri, string defaultSiteName) + public void AddGraphQLClient_AddConfiguredGraphQLClient_To_ServiceCollection(string apiKey, Uri endpointUri, string defaultSiteName) { // Arrange ServiceCollection serviceCollection = []; // Act - serviceCollection.AddGraphQlClient( + serviceCollection.AddGraphQLClient( configuration => { configuration.ApiKey = apiKey; @@ -94,13 +94,13 @@ public void AddGraphQlClient_AddConfiguredGraphQlClient_To_ServiceCollection(str [Theory] [AutoData] - public void AddGraphQlClient_WithContext_To_ServiceCollection(string contextId, Uri endpointUri, string defaultSiteName) + public void AddGraphQLClient_WithContext_To_ServiceCollection(string contextId, Uri endpointUri, string defaultSiteName) { // Arrange ServiceCollection serviceCollection = []; // Act - serviceCollection.AddGraphQlClient( + serviceCollection.AddGraphQLClient( configuration => { configuration.ContextId = contextId; diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/RequestsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/RequestsFixture.cs index 906506e..690693c 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/RequestsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Integration.Tests/RequestsFixture.cs @@ -29,7 +29,7 @@ public async Task ContextRequest_Ok(string handlerName, string contextId) // Set up the client ISitecoreLayoutClientBuilder builder = services.AddSitecoreLayoutService(); - builder.AddGraphQlWithContextHandler(handlerName, contextId).AsDefaultHandler(); + builder.AddGraphQLWithContextHandler(handlerName, contextId).AsDefaultHandler(); // Create an intercept for the actual HTTP call MockHttpMessageHandler result = new(); @@ -72,8 +72,8 @@ public async Task ApiRequest_Ok(string handler1Name, string handler2Name, string // Set up the client ISitecoreLayoutClientBuilder builder = services.AddSitecoreLayoutService(); - builder.AddGraphQlHandler(handler1Name, site1Name, apiKey, endpoint).AsDefaultHandler(); - builder.AddGraphQlHandler(handler2Name, site2Name); + builder.AddGraphQLHandler(handler1Name, site1Name, apiKey, endpoint).AsDefaultHandler(); + builder.AddGraphQLHandler(handler2Name, site2Name); // Create an intercept for the actual HTTP call MockHttpMessageHandler result = new(); diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/LayoutServiceGraphQlExceptionFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/LayoutServiceGraphQlExceptionFixture.cs index 92aef0e..1ed8378 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/LayoutServiceGraphQlExceptionFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Exceptions/LayoutServiceGraphQlExceptionFixture.cs @@ -6,17 +6,17 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Exceptions; -public class LayoutServiceGraphQlExceptionFixture +public class LayoutServiceGraphQLExceptionFixture { [Theory] [AutoNSubstituteData] - public void LayoutServiceGraphQlException_GraphQlError_Get(GraphQLError error) + public void LayoutServiceGraphQLException_GraphQLError_Get(GraphQLError error) { // Arrange - LayoutServiceGraphQlException sut = new(error); + LayoutServiceGraphQLException sut = new(error); // Act - GraphQLError result = sut.GraphQlError; + GraphQLError result = sut.GraphQLError; // Assert result.Should().Be(error); diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs index 1a23a57..d53bad1 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Extensions/SitecoreLayoutClientBuilderExtensionsFixture.cs @@ -168,10 +168,10 @@ public void WithDefaultRequestOptions_RequestDefaultsHasApiKeySpecified_ServiceP [Theory] [AutoNSubstituteData] - public void AddGraphQlHandler_Minimal_IsValid(SitecoreLayoutClientBuilder builder, string name, string siteName, string apiKey, Uri uri) + public void AddGraphQLHandler_Minimal_IsValid(SitecoreLayoutClientBuilder builder, string name, string siteName, string apiKey, Uri uri) { // Act - ILayoutRequestHandlerBuilder result = builder.AddGraphQlHandler(name, siteName, apiKey, uri); + ILayoutRequestHandlerBuilder result = builder.AddGraphQLHandler(name, siteName, apiKey, uri); // Assert ServiceProvider provider = result.Services.BuildServiceProvider(); @@ -183,10 +183,10 @@ public void AddGraphQlHandler_Minimal_IsValid(SitecoreLayoutClientBuilder builde [Theory] [AutoNSubstituteData] - public void AddGraphQlWithContextHandler_Minimal_IsValid(SitecoreLayoutClientBuilder builder, string contextId) + public void AddGraphQLWithContextHandler_Minimal_IsValid(SitecoreLayoutClientBuilder builder, string contextId) { // Act - ILayoutRequestHandlerBuilder result = builder.AddGraphQlWithContextHandler("Test", contextId); + ILayoutRequestHandlerBuilder result = builder.AddGraphQLWithContextHandler("Test", contextId); // Assert ServiceProvider provider = result.Services.BuildServiceProvider(); diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs index c5529c5..c8086b6 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/GraphQlLayoutServiceHandlerFixture.cs @@ -14,18 +14,18 @@ namespace Sitecore.AspNetCore.SDK.LayoutService.Client.Tests.Request.Handlers; -public class GraphQlLayoutServiceHandlerFixture +public class GraphQLLayoutServiceHandlerFixture { private readonly IGraphQLClient _client; private readonly ISitecoreLayoutSerializer _serializer; - private readonly GraphQlLayoutServiceHandler _graphQlLayoutServiceHandler; + private readonly GraphQLLayoutServiceHandler _graphQlLayoutServiceHandler; - public GraphQlLayoutServiceHandlerFixture() + public GraphQLLayoutServiceHandlerFixture() { _client = Substitute.For(); _serializer = Substitute.For(); - ILogger? logger = Substitute.For>(); - _graphQlLayoutServiceHandler = new GraphQlLayoutServiceHandler(_client, _serializer, logger); + ILogger? logger = Substitute.For>(); + _graphQlLayoutServiceHandler = new GraphQLLayoutServiceHandler(_client, _serializer, logger); } [Theory] diff --git a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs index 84c0131..b8af9b1 100644 --- a/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.LayoutService.Client.Tests/Request/Handlers/HttpLayoutRequestHandlerFixture.cs @@ -426,7 +426,7 @@ public async Task Request_WithInvalidHttpHeaders_DoNotPopulatesErrors( SitecoreLayoutRequest request) { // Arrange - var httpResponse = new HttpResponseMessageWrapper(System.Net.HttpStatusCode.OK) + HttpResponseMessageWrapper? httpResponse = new(System.Net.HttpStatusCode.OK) { Headers = null }; diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs index 98665c0..d1db512 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -34,7 +34,7 @@ public class PagesSetupControllerFixture f.Inject(requestDelegate); IOptions pagesOptions = Substitute.For>(); - PagesOptions pagesOptionsValues = new PagesOptions + PagesOptions pagesOptionsValues = new() { ConfigEndpoint = ValidConfigEndpoint, RenderEndpoint = ValidRenderEndpoint, @@ -51,7 +51,7 @@ public class PagesSetupControllerFixture IOptions renderingEngineOptions = Substitute.For>(); string componentName = "TestComponent"; ComponentRendererDescriptor componentRendererDescriptor = new(name => name == componentName, _ => null!, componentName); - RenderingEngineOptions renderingEngineOptionsValues = new RenderingEngineOptions + RenderingEngineOptions renderingEngineOptionsValues = new() { RendererRegistry = new SortedList { diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs index 5bab454..1c67818 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs @@ -74,7 +74,7 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_Placeholder Placeholders = new Dictionary { { - "placeholder_1", new Placeholder() + "placeholder_1", [] } } } @@ -96,8 +96,7 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_NestedPlaceholder Placeholders = new Dictionary { { - "placeholder_1", new Placeholder - { + "placeholder_1", [ new Component { Name = "component_1", @@ -105,11 +104,11 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_NestedPlaceholder Placeholders = new Dictionary { { - "nested_placeholder_1", new Placeholder() + "nested_placeholder_1", [] } } } - } + ] } } } @@ -131,14 +130,13 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_WithComponentInPl Placeholders = new Dictionary { { - "placeholder_1", new Placeholder - { + "placeholder_1", [ new Component { Name = "component_1", Id = "component_1" } - } + ] } } } @@ -160,8 +158,7 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_ComponentInNested Placeholders = new Dictionary { { - "placeholder_1", new Placeholder - { + "placeholder_1", [ new Component { Name = "component_1", @@ -169,18 +166,17 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_ComponentInNested Placeholders = new Dictionary { { - "nested_placeholder_1", new Placeholder - { + "nested_placeholder_1", [ new Component { Name = "nested_component_2", Id = "nested_component_2" } - } + ] } } } - } + ] } } } @@ -202,18 +198,21 @@ public static SitecoreLayoutResponseContent MockLayoutResponse_ComponentWithFiel Placeholders = new Dictionary { { - "placeholder_1", new Placeholder - { + "placeholder_1", [ new Component { Name = "component_1", Id = "component_1", Fields = new Dictionary { - { "field_1", new JsonSerializedField(JsonDocument.Parse("{\"metadata\":{\"datasource\":{\"id\":\"datasource_id\",\"language\":\"en\",\"revision\":\"revision_1\",\"version\":1},\"title\":\"Text\",\"fieldId\":\"field_id\",\"fieldType\":\"Text\",\"rawValue\":\"field_raw_value\"},\"value\":\"field_value\"}")) } + { + "field_1", + new JsonSerializedField(JsonDocument.Parse( + "{\"metadata\":{\"datasource\":{\"id\":\"datasource_id\",\"language\":\"en\",\"revision\":\"revision_1\",\"version\":1},\"title\":\"Text\",\"fieldId\":\"field_id\",\"fieldType\":\"Text\",\"rawValue\":\"field_raw_value\"},\"value\":\"field_value\"}")) + } } } - } + ] } } } diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs index 9c8a92a..5eeda55 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -115,7 +115,7 @@ public async Task Request_NotValidEditingRequest_ErrorThrown(GraphQLEditingServi public async Task Request_NoLanguageSet_ErrorThrown(GraphQLEditingServiceHandler sut) { // Arrange - SitecoreLayoutRequest request = new SitecoreLayoutRequest + SitecoreLayoutRequest request = new() { { "sc_request_headers_key", new Dictionary() diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs index f7f1176..cd0412c 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs @@ -19,14 +19,14 @@ public static GraphQLResponse DictionaryResponseWitho { Dictionary = new SiteInfoDictionary { - Results = new List - { - new SiteInfoDictionaryItem + Results = + [ + new() { Key = "key1", Value = "value1" } - }, + ], PageInfo = new PageInfo { HasNext = false, @@ -54,14 +54,14 @@ public static GraphQLResponse DictionaryResponseWithP { Dictionary = new SiteInfoDictionary { - Results = new List - { - new SiteInfoDictionaryItem + Results = + [ + new() { Key = "page1", Value = "page1" } - }, + ], PageInfo = new PageInfo { HasNext = true, diff --git a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs index c6d7c32..649cecb 100644 --- a/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Reflection; using AutoFixture; using AutoFixture.Idioms; using FluentAssertions; @@ -88,8 +89,8 @@ public async Task GetSiteDictionary_MultiplePageResult_ReturnsCorrectCollection( // Arrange DictionaryService sut = new(pageOptions); IGraphQLClient graphQLClient = Substitute.For(); - graphQLClient.SendQueryAsync(Arg.Is(x => GraphQlQueryHasAfterVariableWithValue(x, string.Empty))).Returns(Constants.DictionaryResponseWithPaging); - graphQLClient.SendQueryAsync(Arg.Is(x => GraphQlQueryHasAfterVariableWithValue(x, "abcd1234"))).Returns(Constants.DictionaryResponseWithoutPaging); + graphQLClient.SendQueryAsync(Arg.Is(x => GraphQLQueryHasAfterVariableWithValue(x, string.Empty))).Returns(Constants.DictionaryResponseWithPaging); + graphQLClient.SendQueryAsync(Arg.Is(x => GraphQLQueryHasAfterVariableWithValue(x, "abcd1234"))).Returns(Constants.DictionaryResponseWithoutPaging); // Act List result = await sut.GetSiteDictionary("valid_site", "valid_language", graphQLClient); @@ -98,7 +99,7 @@ public async Task GetSiteDictionary_MultiplePageResult_ReturnsCorrectCollection( result.Should().HaveCount(2); } - public bool GraphQlQueryHasAfterVariableWithValue(GraphQLRequest graphQlRequst, string expectedAfterValue) + public bool GraphQLQueryHasAfterVariableWithValue(GraphQLRequest graphQlRequst, string expectedAfterValue) { if (!graphQlRequst.ContainsKey("variables")) { @@ -106,13 +107,13 @@ public bool GraphQlQueryHasAfterVariableWithValue(GraphQLRequest graphQlRequst, } Type afterVariable = graphQlRequst["variables"].GetType(); - var afterProperty = afterVariable.GetProperty("after"); + PropertyInfo? afterProperty = afterVariable.GetProperty("after"); if (afterProperty == null) { return false; } - var afterVariableValue = afterProperty.GetValue(graphQlRequst["variables"]); + object? afterVariableValue = afterProperty.GetValue(graphQlRequst["variables"]); if (afterVariableValue == null) { return false; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs index 1483c7e..6621464 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Controllers; @@ -7,7 +8,7 @@ public class PagesController : Controller { public IActionResult Index() { - var context = HttpContext.GetSitecoreRenderingContext(); + ISitecoreRenderingContext? context = HttpContext.GetSitecoreRenderingContext(); return View("~/Views/Shared/HeadlessSxaLayout.cshtml", context); } } \ No newline at end of file diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/AdvanceLocalizationFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/AdvanceLocalizationFixture.cs index 966ee98..3968c72 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/AdvanceLocalizationFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/AdvanceLocalizationFixture.cs @@ -44,7 +44,7 @@ public AdvanceLocalizationFixture() app.UseRouting(); app.UseRequestLocalization(options => { - List supportedCultures = [new CultureInfo("en"), new CultureInfo("da")]; + List supportedCultures = [new("en"), new("da")]; options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationFixture.cs index 9a859bf..7868445 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationFixture.cs @@ -41,7 +41,7 @@ public LocalizationFixture() app.UseRouting(); app.UseRequestLocalization(options => { - List supportedCultures = [new CultureInfo("en"), new CultureInfo("ru-RU")]; + List supportedCultures = [new("en"), new("ru-RU")]; options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en"); options.SupportedCultures = supportedCultures; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationUsingAttributeMiddlewareFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationUsingAttributeMiddlewareFixture.cs index 3556668..e0e6eae 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationUsingAttributeMiddlewareFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Localization/LocalizationUsingAttributeMiddlewareFixture.cs @@ -42,7 +42,7 @@ public LocalizationUsingAttributeMiddlewareFixture() app.UseRouting(); app.UseRequestLocalization(options => { - List supportedCultures = [new CultureInfo("en"), new CultureInfo("uk-UA"), new CultureInfo("da-DK")]; + List supportedCultures = [new("en"), new("uk-UA"), new("da-DK")]; options.DefaultRequestCulture = new RequestCulture(culture: "en", uiCulture: "en"); options.SupportedCultures = supportedCultures; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Multisite/MultisiteFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Multisite/MultisiteFixture.cs index 2bd160e..55627d9 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Multisite/MultisiteFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Multisite/MultisiteFixture.cs @@ -42,8 +42,8 @@ public MultisiteFixture() .AddHttpHandler("mock", _ => new HttpClient(_mockClientHandler) { BaseAddress = _layoutServiceUri }) .AsDefaultHandler(); - IGraphQLClient? mockedGraphQlClient = Substitute.For(); - mockedGraphQlClient.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse + IGraphQLClient? mockedGraphQLClient = Substitute.For(); + mockedGraphQLClient.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse { Data = new SiteInfoCollectionResult { @@ -67,7 +67,7 @@ public MultisiteFixture() options.AddDefaultPartialView("_ComponentNotFound"); }); - builder.AddSingleton(mockedGraphQlClient); + builder.AddSingleton(mockedGraphQLClient); builder.AddMultisite(); }) .Configure(app => diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs index b697b8b..0878a8a 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesEditingFixture.cs @@ -11,21 +11,21 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Pag public class PagesEditingFixture(TestWebApplicationFactory factory) : IClassFixture> { - private readonly TestWebApplicationFactory factory = factory; + private readonly TestWebApplicationFactory _factory = factory; [Fact] public async Task EditingRequest_ValidRequest_ReturnsChromeDecoratedResponse() { // Arrange - factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.SimpleEditingLayoutQueryResponse); - factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.DictionaryResponseWithoutPaging); + _factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.SimpleEditingLayoutQueryResponse); + _factory.MockGraphQLClient.SendQueryAsync(Arg.Any()).Returns(TestConstants.DictionaryResponseWithoutPaging); - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"/Pages/index?mode=edit&secret={TestConstants.JssEditingSecret}&sc_itemid={TestConstants.TestItemId}&sc_version=1&sc_layoutKind=final"; // Act - var response = await client.GetAsync(url); - var responseBody = await response.Content.ReadAsStringAsync(); + HttpResponseMessage? response = await client.GetAsync(url); + string? responseBody = await response.Content.ReadAsStringAsync(); // Assert response.Should().NotBeNull(); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs index b6953f9..72c3691 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs @@ -7,17 +7,17 @@ namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Fixtures.Pag public class PagesSetupRoutingFixture(TestWebApplicationFactory factory) : IClassFixture> { - private readonly TestWebApplicationFactory factory = factory; + private readonly TestWebApplicationFactory _factory = factory; [Fact] public async Task ConfigRoute_MissingSecret_ReturnsBadRequest() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.ConfigRoute}?secret="; // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -28,11 +28,11 @@ public async Task ConfigRoute_MissingSecret_ReturnsBadRequest() public async Task ConfigRoute_InvalidSecret_ReturnsBadRequest() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.ConfigRoute}?secret=invalid_secret_value"; // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -43,12 +43,12 @@ public async Task ConfigRoute_InvalidSecret_ReturnsBadRequest() public async Task ConfigRoute_InvalidRequestOrigin_ReturnsBadRequest() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.ConfigRoute}?secret={TestConstants.JssEditingSecret}"; client.DefaultRequestHeaders.Add("Origin", "http://invalid_origin_domain.com"); // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -59,12 +59,12 @@ public async Task ConfigRoute_InvalidRequestOrigin_ReturnsBadRequest() public async Task ConfigRoute_ValidCall_ReturnsCorrectObject() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.ConfigRoute}?secret={TestConstants.JssEditingSecret}"; client.DefaultRequestHeaders.Add("Origin", "https://pages.sitecorecloud.io"); // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -80,11 +80,11 @@ public async Task ConfigRoute_ValidCall_ReturnsCorrectObject() public async Task RenderRoute_MissingSecret_ReturnsBadRequest() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.RenderRoute}?secret="; // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -95,11 +95,11 @@ public async Task RenderRoute_MissingSecret_ReturnsBadRequest() public async Task RenderRoute_InvalidSecret_ReturnsBadRequest() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); string url = $"{TestConstants.RenderRoute}?secret=invalid_secret_value"; // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); @@ -110,7 +110,7 @@ public async Task RenderRoute_InvalidSecret_ReturnsBadRequest() public async Task RenderRoute_ValidCall_ReturnsCorrectResponse() { // Arrange - HttpClient client = factory.CreateClient(); + HttpClient client = _factory.CreateClient(); Guid itemId = Guid.NewGuid(); string language = "en"; string layoutKind = "final"; @@ -122,7 +122,7 @@ public async Task RenderRoute_ValidCall_ReturnsCorrectResponse() string url = $"{TestConstants.RenderRoute}?secret={TestConstants.JssEditingSecret}&sc_itemid={itemId}&sc_lang={language}&sc_layoutKind={layoutKind}&mode={mode}&sc_site={site}&sc_version={version}&tenant_id={tenantId}&route={route}"; // Act - var response = await client.GetAsync(url); + HttpResponseMessage? response = await client.GetAsync(url); // Assert response.Should().NotBeNull(); diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs index cf65cf0..7b65ba8 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs @@ -10,14 +10,14 @@ builder.Services.AddRouting() .AddMvc(); -builder.Services.AddGraphQlClient(configuration => +builder.Services.AddGraphQLClient(configuration => { configuration.ContextId = TestConstants.ContextId; }); builder.Services.AddSitecoreLayoutService() .AddSitecorePagesHandler() - .AddGraphQlWithContextHandler("default", TestConstants.ContextId!, siteName: TestConstants.SiteName!) + .AddGraphQLWithContextHandler("default", TestConstants.ContextId!, siteName: TestConstants.SiteName!) .AsDefaultHandler(); builder.Services.AddSitecoreRenderingEngine(options => diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs index 1d9885c..8d647c1 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/SearchOptimization/EdgeSitemapProxyFixture.cs @@ -39,8 +39,8 @@ public EdgeSitemapProxyFixture() new HttpClient(_mockClientHandler)); }); - IGraphQLClient? mockedGraphQlClient = Substitute.For(); - mockedGraphQlClient.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse + IGraphQLClient? mockedGraphQLClient = Substitute.For(); + mockedGraphQLClient.SendQueryAsync(Arg.Any()).Returns(new GraphQLResponse { Data = new SiteInfoResultModel { @@ -57,7 +57,7 @@ public EdgeSitemapProxyFixture() } }); - builder.AddSingleton(mockedGraphQlClient); + builder.AddSingleton(mockedGraphQLClient); builder.AddEdgeSitemap(); }) .Configure(app => diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs index c6cc69b..9f5830a 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Extensions/MultisiteAppConfigurationExtensionsFixture.cs @@ -41,7 +41,7 @@ public void AddSitecoreRedirects_RegisterProperServicesAndConfiguration(Action(string.Empty, multisiteOptions)); serviceCollection[6].ServiceType.Should().Be(typeof(ISiteCollectionService)); - serviceCollection[6].ImplementationType.Should().Be(typeof(GraphQlSiteCollectionService)); + serviceCollection[6].ImplementationType.Should().Be(typeof(GraphQLSiteCollectionService)); serviceCollection[7].ServiceType.Should().Be(typeof(ISiteResolver)); serviceCollection[7].ImplementationType.Should().Be(typeof(SiteResolver)); } diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs index dbb3763..6a23263 100644 --- a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/Services/SiteResolverFixture.cs @@ -21,10 +21,10 @@ public async Task GetByHost_ResolvesByHost_WithMultipleHostNames_AndWhitespaces( // Arrange List siteCollection = [ - new SiteInfo { Name = "site1", HostName = "*.eu.site.com" }, - new SiteInfo { Name = "site2", HostName = "*.SITE.com | longsitehost.com" }, - new SiteInfo { Name = "site3", HostName = "ordeR.eu.site.com | longsitehost2.com" }, - new SiteInfo { Name = "site4", HostName = "i.site.com" }, + new() { Name = "site1", HostName = "*.eu.site.com" }, + new() { Name = "site2", HostName = "*.SITE.com | longsitehost.com" }, + new() { Name = "site3", HostName = "ordeR.eu.site.com | longsitehost2.com" }, + new() { Name = "site4", HostName = "i.site.com" }, ]; _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); @@ -43,8 +43,8 @@ public async Task GetByHost_ShouldReturnSiteWhenWildcardIsProvided() // Arrange List siteCollection = [ - new SiteInfo { Name = "bar", HostName = "bar.net" }, - new SiteInfo { Name = "wildcard", HostName = "*" } + new() { Name = "bar", HostName = "bar.net" }, + new() { Name = "wildcard", HostName = "*" } ]; _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); @@ -63,10 +63,10 @@ public async Task GetByHost_ShouldPreferMostSpecificMatch() // Arrange List siteCollection = [ - new SiteInfo { Name = "foo", HostName = "*" }, - new SiteInfo { Name = "bar", HostName = "*.app.net" }, - new SiteInfo { Name = "i-bar", HostName = "i.app.net" }, - new SiteInfo { Name = "baz", HostName = "baz.app.net" } + new() { Name = "foo", HostName = "*" }, + new() { Name = "bar", HostName = "*.app.net" }, + new() { Name = "i-bar", HostName = "i.app.net" }, + new() { Name = "baz", HostName = "baz.app.net" } ]; _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); @@ -91,10 +91,10 @@ public async Task GetByHost_ShouldPreferFirstSiteMatchForSameHostName() // Arrange List siteCollection = [ - new SiteInfo { Name = "foo", HostName = "*" }, - new SiteInfo { Name = "bar", HostName = "Bar.net" }, - new SiteInfo { Name = "foo-never", HostName = "*" }, - new SiteInfo { Name = "bar-never", HostName = "bar.net" } + new() { Name = "foo", HostName = "*" }, + new() { Name = "bar", HostName = "Bar.net" }, + new() { Name = "foo-never", HostName = "*" }, + new() { Name = "bar-never", HostName = "bar.net" } ]; _siteCollectionService.GetSitesCollection().Returns([.. siteCollection]); diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs index da2f60f..5a2c509 100644 --- a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs +++ b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs @@ -1,9 +1,9 @@ -using GraphQL; -using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; -using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Json; +using GraphQL; +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; namespace Sitecore.AspNetCore.SDK.TestData; @@ -116,20 +116,20 @@ public static GraphQLResponse DictionaryResponseWitho { Data = new EditingDictionaryResponse { - Site = new Pages.Request.Handlers.GraphQL.Site + Site = new Site { SiteInfo = new SiteInfo { Dictionary = new SiteInfoDictionary { - Results = new List + Results = + [ + new SiteInfoDictionaryItem() { - new SiteInfoDictionaryItem - { - Key = "key1", - Value = "value1" - } - }, + Key = "key1", + Value = "value1" + } + ], PageInfo = new PageInfo { HasNext = false, @@ -143,6 +143,7 @@ public static GraphQLResponse DictionaryResponseWitho } } +#pragma warning disable SA1201 #pragma warning disable SA1401 #pragma warning disable CA2211 public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; @@ -150,4 +151,5 @@ public static GraphQLResponse DictionaryResponseWitho public static DateTime DateTimeValue = DateTime.ParseExact("2012-05-04T00:00:00Z", "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); #pragma warning restore CA2211 #pragma warning restore SA1401 +#pragma warning restore SA1201 } \ No newline at end of file From 64cb3d7210b4ef7a13c83a3995663aa4891a347e Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 27 May 2025 08:51:01 +1000 Subject: [PATCH 36/38] Fixed incorrect error being thrown from Config call --- .../Controllers/PagesSetupController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs index 9b8dab5..7b6c90f 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -131,7 +131,7 @@ private bool IsValidEditingSecret(HttpRequest httpRequest) if (!result) { - _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingOrigin); + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingSecretValue); } return result; From 8cfecbf190c6b326c03d994da31d78eb6e6f8f0f Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 27 May 2025 13:27:11 +1000 Subject: [PATCH 37/38] Changes from PR Review --- .../Constants.cs | 53 +++++++++++++++++++ .../PagesAppConfigurationExtensions.cs | 18 +++---- .../GraphQL/GraphQLEditingServiceHandler.cs | 21 ++------ .../Services/DictionaryService.cs | 24 +-------- .../Sitecore.AspNetCore.SDK.Pages.csproj | 6 --- 5 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs index e5f1499..51b2644 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs @@ -76,6 +76,59 @@ public static class QueryStringKeys /// Tenant id query string key. ///
public const string TenantId = "tenant_id"; + + /// + /// Sitecore Edit mode query string key. + /// + public const string EditMode = "sc_editmode"; + } + + /// + /// Class used to store the GraphQL queries used by the Pages project. + /// + public static class GraphQlQueries + { + /// + /// Defines a constant string for an editing layout request. + /// + public const string EditingLayoutRequest = @" + query EditingQuery( + $itemId: String!, + $language: String!, + $version: String + ) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + } + "; + + /// + /// Define a constant string for an editing dictionary request. + /// + public const string EditingDictionaryRequest = @" + query DictionaryQuery( + $siteName: String! + $language: String! + $after: String + $pageSize: Int + ) { + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + "; } } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs index ca820c4..b965b0b 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -86,15 +86,15 @@ public static ISitecoreRenderingEngineBuilder WithSitecorePages(this ISitecoreRe { renderingOptions.MapToRequest((httpRequest, layoutRequest) => { - MapRequest(httpRequest, layoutRequest, "mode"); - MapRequest(httpRequest, layoutRequest, "sc_itemid"); - MapRequest(httpRequest, layoutRequest, "sc_version"); - MapRequest(httpRequest, layoutRequest, "sc_lang"); - MapRequest(httpRequest, layoutRequest, "sc_site"); - MapRequest(httpRequest, layoutRequest, "sc_layoutKind"); - MapRequest(httpRequest, layoutRequest, "secret"); - MapRequest(httpRequest, layoutRequest, "tenant_id"); - MapRequest(httpRequest, layoutRequest, "route"); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Mode); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.ItemId); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Version); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Language); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Site); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.LayoutKind); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Secret); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.TenantId); + MapRequest(httpRequest, layoutRequest, Constants.QueryStringKeys.Route); }); })); diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index 332334b..f0a13b4 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -11,7 +11,6 @@ using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization; using Sitecore.AspNetCore.SDK.LayoutService.Client.Serialization.Fields; -using Sitecore.AspNetCore.SDK.Pages.Configuration; using Sitecore.AspNetCore.SDK.Pages.Properties; using Sitecore.AspNetCore.SDK.Pages.Services; @@ -271,28 +270,18 @@ private static GraphQLHttpRequestWithHeaders BuildEditingLayoutRequest(SitecoreL { return new() { - Query = @" - query EditingQuery( - $itemId: String!, - $language: String!, - $version: String - ) { - item(path: $itemId, language: $language, version: $version) { - rendered - } - } - ", + Query = Constants.GraphQlQueries.EditingLayoutRequest, OperationName = "EditingQuery", Variables = new { - itemId = GetRequestArgValue(request, "sc_itemid"), + itemId = GetRequestArgValue(request, Constants.QueryStringKeys.ItemId), language = requestLanguage, - version = GetRequestArgValue(request, "sc_version") + version = GetRequestArgValue(request, Constants.QueryStringKeys.Version) }, Headers = new Dictionary { - { "sc_layoutKind", GetRequestArgValue(request, "sc_layoutKind") }, - { "sc_editmode", (GetRequestArgValue(request, "mode") == "edit").ToString() } + { Constants.QueryStringKeys.LayoutKind, GetRequestArgValue(request, Constants.QueryStringKeys.LayoutKind) }, + { Constants.QueryStringKeys.EditMode, (GetRequestArgValue(request, Constants.QueryStringKeys.Mode) == "edit").ToString() } } }; } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs index 76938cf..dee097e 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs @@ -50,29 +50,7 @@ private GraphQLRequest BuildEditingDictionaryRequest(string siteName, string req { return new() { - Query = @" - query DictionaryQuery( - $siteName: String! - $language: String! - $after: String - $pageSize: Int - ) { - site { - siteInfo(site: $siteName) { - dictionary(language: $language, first: $pageSize, after: $after) { - results { - key - value - } - pageInfo { - endCursor - hasNext - } - } - } - } - } - ", + Query = Constants.GraphQlQueries.EditingDictionaryRequest, OperationName = "DictionaryQuery", Variables = new { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj index 5efbcf4..f03cfef 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj +++ b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj @@ -1,11 +1,5 @@  - - net8.0 - enable - enable - - From 7e2b89376c9a0ad589e560aa38c44a73cfbb3ea7 Mon Sep 17 00:00:00 2001 From: Rob Earlam Date: Tue, 27 May 2025 17:38:38 +1000 Subject: [PATCH 38/38] Moved MetaData Queries back from constants to improve readability --- .../Constants.cs | 48 ------------------- .../GraphQL/GraphQLEditingServiceHandler.cs | 12 ++++- .../Services/DictionaryService.cs | 24 +++++++++- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs index 51b2644..19c1c41 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs @@ -82,53 +82,5 @@ public static class QueryStringKeys ///
public const string EditMode = "sc_editmode"; } - - /// - /// Class used to store the GraphQL queries used by the Pages project. - /// - public static class GraphQlQueries - { - /// - /// Defines a constant string for an editing layout request. - /// - public const string EditingLayoutRequest = @" - query EditingQuery( - $itemId: String!, - $language: String!, - $version: String - ) { - item(path: $itemId, language: $language, version: $version) { - rendered - } - } - "; - - /// - /// Define a constant string for an editing dictionary request. - /// - public const string EditingDictionaryRequest = @" - query DictionaryQuery( - $siteName: String! - $language: String! - $after: String - $pageSize: Int - ) { - site { - siteInfo(site: $siteName) { - dictionary(language: $language, first: $pageSize, after: $after) { - results { - key - value - } - pageInfo { - endCursor - hasNext - } - } - } - } - } - "; - } } } diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs index f0a13b4..25f10a1 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -270,7 +270,17 @@ private static GraphQLHttpRequestWithHeaders BuildEditingLayoutRequest(SitecoreL { return new() { - Query = Constants.GraphQlQueries.EditingLayoutRequest, + Query = @" + query EditingQuery( + $itemId: String!, + $language: String!, + $version: String + ) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + } + ", OperationName = "EditingQuery", Variables = new { diff --git a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs index dee097e..9153f88 100644 --- a/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs @@ -50,7 +50,29 @@ private GraphQLRequest BuildEditingDictionaryRequest(string siteName, string req { return new() { - Query = Constants.GraphQlQueries.EditingDictionaryRequest, + Query = @" + query DictionaryQuery( + $siteName: String! + $language: String! + $after: String + $pageSize: Int + ) { + site { + siteInfo(site: $siteName) { + dictionary(language: $language, first: $pageSize, after: $after) { + results { + key + value + } + pageInfo { + endCursor + hasNext + } + } + } + } + } + ", OperationName = "DictionaryQuery", Variables = new {