diff --git a/Sitecore.AspNetCore.SDK.sln b/Sitecore.AspNetCore.SDK.sln index fd5cc1b..bb970c2 100644 --- a/Sitecore.AspNetCore.SDK.sln +++ b/Sitecore.AspNetCore.SDK.sln @@ -112,6 +112,10 @@ 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 +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 @@ -186,6 +190,14 @@ 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 + {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 @@ -221,6 +233,8 @@ 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} + {55601B5C-5D9C-66E5-801D-E5D5EA0E29D6} = {BDE3D3B9-8291-4AE9-B8DA-868CEBCBDC4D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2E4F7126-B772-42CB-8F90-93B221ED0A72} 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.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.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.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..ae428a6 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,15 @@ public class EditableField [DataMember(Name = "editable")] [JsonPropertyName("editable")] public string EditableMarkup { 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..14e158d 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,14 @@ 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..fd83109 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.LayoutService.Client/Response/Model/MetaData.cs @@ -0,0 +1,32 @@ +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/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..2d41bb5 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Configuration/PagesOptions.cs @@ -0,0 +1,52 @@ +namespace Sitecore.AspNetCore.SDK.Pages.Configuration; + +/// +/// The options to configure the Pages middleware. +/// +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. + /// + 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 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; + + /// + /// 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/Constants.cs b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs new file mode 100644 index 0000000..19c1c41 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Constants.cs @@ -0,0 +1,86 @@ +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"; + } + + /// + /// 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"; + + /// + /// Sitecore Edit mode query string key. + /// + public const string EditMode = "sc_editmode"; + } + } +} 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..7b6c90f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Controllers/PagesSetupController.cs @@ -0,0 +1,155 @@ +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.Models; +using Sitecore.AspNetCore.SDK.Pages.Properties; +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 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)); + + /// + /// The Config endpoint used to inform Pages how this editing host is configured and which components are implemented. + /// + /// PagesConfigResponse object. + [HttpGet] + [HttpOptions] + public ActionResult Config() + { + if (IsValidPagesConfigRequest(Request)) + { + _logger.LogDebug("{Message}", Resources.Debug_ProcessingValidPagesConfigRequest); + 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() + { + IActionResult result; + if (IsValidPagesRenderRequest(Request)) + { + _logger.LogDebug("{Message}", Resources.Debug_ProcessingValidPagesRenderRequest); + PagesRenderArgs args = ParseQueryStringArgs(Request); + 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 result; + } + + private static PagesRenderArgs ParseQueryStringArgs(HttpRequest request) + { + return new PagesRenderArgs + { + 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) + { + bool result = false; + if (IsValidEditingSecret(httpRequest)) + { + result = true; + } + else + { + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingSecretValue); + } + + return result; + } + + private PagesConfigResponse BuildConfigResponseBody() + { + return new PagesConfigResponse + { + EditMode = "metadata", + 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 = _options.ValidMethods; + httpResponse.Headers.AccessControlAllowHeaders = _options.ValidHeaders; + httpResponse.StatusCode = StatusCodes.Status200OK; + httpResponse.ContentType = "application/json"; + } + + private bool IsValidPagesConfigRequest(HttpRequest httpRequest) + { + return IsValidEditingSecret(httpRequest) && RequestHasValidEditingOrigin(httpRequest); + } + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + bool result = false; + if (httpRequest.Query.TryGetValue(Constants.QueryStringKeys.Secret, out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == _options.EditingSecret) + { + result = true; + } + } + + if (!result) + { + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingSecretValue); + } + + return result; + } + + private bool RequestHasValidEditingOrigin(HttpRequest httpRequest) + { + bool result = false; + if (httpRequest.Headers.Origin == _options.ValidEditingOrigin) + { + result = true; + } + else + { + _logger.LogError("{Message}", Resources.Error_InvalidPagesEditingOrigin); + } + + 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 new file mode 100644 index 0000000..b965b0b --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Extensions/PagesAppConfigurationExtensions.cs @@ -0,0 +1,138 @@ +using GraphQL.Client.Abstractions; +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.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; + +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 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) + { + ArgumentNullException.ThrowIfNull(app); + + object? pagesMarker = app.Services.GetService(typeof(PagesMarkerService)); + if (pagesMarker != null) + { + 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; + } + + /// + /// 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, string contextId, Action? options = null) + { + ArgumentNullException.ThrowIfNull(serviceBuilder); + + IServiceCollection services = serviceBuilder.Services; + if (services.Any(s => s.ServiceType == typeof(PagesMarkerService))) + { + return serviceBuilder; + } + + services.AddSingleton(); + services.AddSingleton(); + + if (options != null) + { + services.Configure(options); + } + + services.Configure((Action)(renderingOptions => + { + renderingOptions.MapToRequest((httpRequest, layoutRequest) => + { + 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); + }); + })); + + 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)) + { + 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 new file mode 100644 index 0000000..48330bb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Middleware/PagesRenderMiddleware.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +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.Properties; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; +using Sitecore.AspNetCore.SDK.RenderingEngine.Rendering; + +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 map the HttpRequest to a Layout Service request. +/// The layout service client. +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 ISitecoreLayoutClient _layoutService = layoutService ?? throw new ArgumentNullException(nameof(layoutService)); + + /// + /// The middleware Invoke method. + /// + /// The current . + /// The current . + /// The current . + /// A Task to support async calls. + public async Task Invoke(HttpContext httpContext, IViewComponentHelper viewComponentHelper, IHtmlHelper htmlHelper) + { + ArgumentNullException.ThrowIfNull(httpContext); + ArgumentNullException.ThrowIfNull(viewComponentHelper); + ArgumentNullException.ThrowIfNull(htmlHelper); + + if (IsValidEditingRequest(httpContext)) + { + if (httpContext.Items.ContainsKey(nameof(PagesRenderMiddleware))) + { + throw new ApplicationException(Resources.Exception_PagesRenderMiddlewareAlreadyRegistered); + } + + if (httpContext.GetSitecoreRenderingContext() == null) + { + SitecoreLayoutResponse response = await GetSitecoreLayoutResponse(httpContext).ConfigureAwait(false); + + SitecoreRenderingContext scContext = new() + { + Response = response, + RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper) + }; + + httpContext.SetSitecoreRenderingContext(scContext); + } + else + { + ISitecoreRenderingContext? scContext = httpContext.GetSitecoreRenderingContext(); + if (scContext != null) + { + scContext.RenderingHelpers = new RenderingHelpers(viewComponentHelper, htmlHelper); + } + } + + httpContext.Items.Add(nameof(PagesRenderMiddleware), null); + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static bool IsInEditMode(HttpContext context) + { + return context.Request.Query.TryGetValue(Constants.QueryStringKeys.Mode, out StringValues mode) + && mode == "edit"; + } + + private bool IsValidEditingRequest(HttpContext context) + { + return + context.Request.Path != _options.RenderEndpoint + && IsInEditMode(context) + && IsValidEditingSecret(context.Request); + } + + private bool IsValidEditingSecret(HttpRequest httpRequest) + { + bool result = false; + if (httpRequest.Query.TryGetValue(Constants.QueryStringKeys.Secret, out StringValues editingSecretValues)) + { + string editingSecret = editingSecretValues.FirstOrDefault() ?? string.Empty; + if (editingSecret == _options.EditingSecret) + { + result = true; + } + } + + 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); + } +} \ 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 new file mode 100644 index 0000000..b44b9a1 --- /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; } = []; + + /// + /// 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/Models/PagesRenderArgs.cs b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs new file mode 100644 index 0000000..afce819 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Models/PagesRenderArgs.cs @@ -0,0 +1,53 @@ +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 language 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 rendering 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; + + /// + /// 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.Pages/Properties/Resources.Designer.cs b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs new file mode 100644 index 0000000..9ed28bb --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.Designer.cs @@ -0,0 +1,162 @@ +//------------------------------------------------------------------------------ +// +// 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); + } + } + + /// + /// 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 new file mode 100644 index 0000000..fb4411d --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Properties/Resources.resx @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 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/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 new file mode 100644 index 0000000..cd5f886 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/EditingLayoutQueryResponse.cs @@ -0,0 +1,19 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Request.Handlers.GraphQL; + +namespace Sitecore.AspNetCore.SDK.Pages.Request.Handlers.GraphQL; + +/// +/// Layout Service GraphQL Response. +/// +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; } = 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 new file mode 100644 index 0000000..25f10a1 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/GraphQLEditingServiceHandler.cs @@ -0,0 +1,341 @@ +using System.Text.Json; +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; +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.Properties; +using Sitecore.AspNetCore.SDK.Pages.Services; + +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 serializer to handle response data. +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)); + + /// + public async Task Request(SitecoreLayoutRequest request, string handlerName) + { + ArgumentNullException.ThrowIfNull(request); + ArgumentException.ThrowIfNullOrWhiteSpace(handlerName); + + if (!IsEditingRequest(request)) + { + throw new ArgumentException(Resources.Exception_ErrorAttemptingToProcessNonEditingRequest); + } + + 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) + { + bool result = false; + if (request.TryGetHeadersCollection(out Dictionary? headers)) + { + if (headers != null && headers.TryGetValue(Constants.QueryStringKeys.Mode, out string[]? value)) + { + result = value.Contains("edit"); + } + } + + return result; + } + + private static void GenerateMetaDataChromes(SitecoreLayoutResponseContent? content) + { + if (content?.Sitecore?.Route == null) + { + return; + } + + foreach (KeyValuePair 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 result = []; + + // Create a separate class outside of this method for the work item + // to avoid nested class compilation issues + Stack workStack = new(); + workStack.Push(new PlaceholderWorkItem(name, id, placeholderFeatures, result)); + + while (workStack.Count > 0) + { + 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 (IPlaceholderFeature feature in current.Features) + { + if (feature is Component component) + { + // Add opening chrome + AddRenderingOpeningChrome(output, component); + + // Process fields + Dictionary updatedFields = new(); + foreach (KeyValuePair field in component.Fields) + { + ProcessField(updatedFields, field); + } + + component.Fields = updatedFields; + + // Add the component to the output + output.Add(component); + + // Process component placeholders before adding closing chrome + if (component.Placeholders.Count > 0) + { + // For each placeholder in the component, add it to the work stack + 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 = []; + + // Add a work item to process this placeholder + workStack.Push(new PlaceholderWorkItem( + placeholderKey, + component.Id, + placeholderValue, + processedPlaceholder, + component)); + + // Store the processed placeholder for later assignment + component.Placeholders[placeholderKey] = processedPlaceholder; + } + } + + // Add closing chrome for the component + AddRenderingClosingChrome(output); + } + } + + // Add closing chrome for placeholder + AddPlaceholderClosingChrome(output); + } + + return result; + } + + 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) + { + EditableField? editableField; + if (field.Value is JsonSerializedField serialisedField + && field.Key != "CustomContent" && + serialisedField.TryRead(out editableField) + && editableField != null) + { + 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); + + JsonDocument editableFieldWithChromesJson = JsonSerializer.SerializeToDocument(editableField); + JsonSerializedField updatedJsonSerialisedField = new(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() + { + 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 static GraphQLHttpRequestWithHeaders BuildEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage) + { + return new() + { + Query = @" + query EditingQuery( + $itemId: String!, + $language: String!, + $version: String + ) { + item(path: $itemId, language: $language, version: $version) { + rendered + } + } + ", + OperationName = "EditingQuery", + Variables = new + { + itemId = GetRequestArgValue(request, Constants.QueryStringKeys.ItemId), + language = requestLanguage, + version = GetRequestArgValue(request, Constants.QueryStringKeys.Version) + }, + Headers = new Dictionary + { + { Constants.QueryStringKeys.LayoutKind, GetRequestArgValue(request, Constants.QueryStringKeys.LayoutKind) }, + { Constants.QueryStringKeys.EditMode, (GetRequestArgValue(request, Constants.QueryStringKeys.Mode) == "edit").ToString() } + } + }; + } + + private async Task HandleEditingLayoutRequest(SitecoreLayoutRequest request, string requestLanguage, List errors) + { + 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); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug(Resources.Debug_LayoutServiceGraphQLResponse, 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); + + GenerateMetaDataChromes(content); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + object? formattedDeserializeObject = JsonSerializer.Deserialize(json); + _logger.LogDebug(Resources.Debug_LayoutServiceResponseJSON, formattedDeserializeObject); + } + } + + if (response.Errors != null) + { + 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/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/PlaceholderWorkItem.cs b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs new file mode 100644 index 0000000..ee7883f --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Request/Handlers/GraphQL/PlaceholderWorkItem.cs @@ -0,0 +1,44 @@ +using Sitecore.AspNetCore.SDK.LayoutService.Client.Response.Model; + +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. +/// 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. +public class PlaceholderWorkItem( + string placeholderKey, + string id, + Placeholder features, + Placeholder output, + Component? parentComponent = null) +{ + /// + /// Gets the PlaceholderKey of an entity. It is a read-only property initialized with the value of the 'PlaceholderKey' variable. + /// + public string PlaceholderKey { get; } = placeholderKey; + + /// + /// 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; +} 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..4d9061e --- /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; } = 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 new file mode 100644 index 0000000..45f540e --- /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; } = new(); +} 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; +} 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..e46d915 --- /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 variant being edited. + /// + [JsonPropertyName("variant")] + public string? Variant { 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..c257897 --- /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/Services/DictionaryService.cs b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs new file mode 100644 index 0000000..9153f88 --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Services/DictionaryService.cs @@ -0,0 +1,86 @@ +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 = []; + GraphQLResponse dictionaryPageResponse = await GetSinglePageOfDictionaryItems(siteName, requestLanguage, client, dictionary, string.Empty).ConfigureAwait(false); + + 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); + } + + 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/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..f03cfef --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/Sitecore.AspNetCore.SDK.Pages.csproj @@ -0,0 +1,23 @@ + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + 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..bb22efd --- /dev/null +++ b/src/Sitecore.AspNetCore.SDK.Pages/TagHelpers/EditingScriptsTagHelper.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +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; + +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 void Process(TagHelperContext context, TagHelperOutput output) + { + ISitecoreRenderingContext renderingContext = ViewContext?.HttpContext.GetSitecoreRenderingContext() ?? + throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperSitecoreRenderingContextNull); + + output.TagName = string.Empty; + string html = string.Empty; + + if (renderingContext.Response?.Content?.Sitecore?.Context?.IsEditing ?? false) + { + EditingContext? editingContext = JsonSerializer.Deserialize(renderingContext.Response?.Content.ContextRawData ?? string.Empty); + if (editingContext == null) + { + throw new NullReferenceException(Resources.Exception_EditingScriptsTagHelperUnableToProcessContextRawData); + } + + foreach (string script in editingContext.ClientScripts ?? []) + { + 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/Extensions/RenderingEngineOptionsExtensions.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Extensions/RenderingEngineOptionsExtensions.cs index ea0c681..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); @@ -223,7 +229,8 @@ public static RenderingEngineOptions AddModelBoundView( sp => ActivatorUtilities.CreateInstance>( sp, RenderingEngineConstants.SitecoreViewComponents.DefaultSitecoreViewComponentName, - viewName)); + viewName), + sitecoreComponentName); options.RendererRegistry.Add(options.RendererRegistry.Count, descriptor); @@ -242,7 +249,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..679ba70 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ComponentRendererDescriptor.cs @@ -8,9 +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) + Func factory, + string componentName = "") { private readonly Func _factory = factory ?? throw new ArgumentNullException(nameof(factory)); private readonly object _lock = new(); @@ -22,6 +24,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/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); } /// diff --git a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs index 73dc497..174ebf0 100644 --- a/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs +++ b/src/Sitecore.AspNetCore.SDK.RenderingEngine/Rendering/ViewComponentComponentRenderer.cs @@ -29,15 +29,17 @@ 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); return new ComponentRendererDescriptor( match, - sp => ActivatorUtilities.CreateInstance(sp, locator)); + sp => ActivatorUtilities.CreateInstance(sp, locator), + componentName); } /// 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 c52a0c9..a0ef61a 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 ?? throw new ArgumentNullException(nameof(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..84adca1 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 ?? throw new ArgumentNullException(nameof(chromeRenderer)); /// /// Gets or sets the model value. @@ -64,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; } @@ -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..b5dfef4 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,56 +75,6 @@ public override void Process(TagHelperContext context, TagHelperOutput output) } } - private static void RenderMarkup(TagHelperOutput output, HyperLinkField field) - { - if (output.TagName == null) - { - // generate full anchor markup - 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 static 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); - } - /// /// Generates anchor HTML tag. /// @@ -200,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/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs b/src/Sitecore.AspNetCore.SDK.RenderingEngine/TagHelpers/Fields/NumberTagHelper.cs index 6ac8f57..f535d1b 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 ?? throw new ArgumentNullException(nameof(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..da91a54 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 ?? throw new ArgumentNullException(nameof(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..e803b53 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 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.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 ?? throw new ArgumentNullException(nameof(chromeRenderer)); + /// /// Gets or sets the model value. /// @@ -42,14 +46,38 @@ 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) + { + html += _chromeRenderer.Render(field.OpeningChrome); + html += "
"; + isHtml = true; + } + if (outputEditableMarkup || (ConvertNewLines && NewLineRegex().IsMatch(value))) { - value = NewLineRegex().Replace(value, "
"); - output.Content.SetHtmlContent(value); + html += NewLineRegex().Replace(value, "
"); + isHtml = true; + } + else + { + html += value; + } + + if (Editable && field.ClosingChrome != null) + { + html += "
"; + html += _chromeRenderer.Render(field.ClosingChrome); + } + + if (isHtml) + { + output.Content.SetHtmlContent(html); } else { - output.Content.SetContent(value); + output.Content.SetContent(html); } } 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/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.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs b/tests/Sitecore.AspNetCore.SDK.GraphQL.Tests/Request/GraphQLHttpRequestWithHeadersFixture.cs new file mode 100644 index 0000000..054eeeb --- /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.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 new file mode 100644 index 0000000..d1db512 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Controllers/PagesSetupControllerFixture.cs @@ -0,0 +1,228 @@ +using System.Diagnostics.CodeAnalysis; +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; +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.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.Controllers +{ + public class PagesSetupControllerFixture + { + 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() + { + 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() + { + 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 void ConfigRoute_InvalidEditingSecret_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + 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 + 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>()); + response.Result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void ConfigRoute_InvalidEditingOrigin_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + 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 + ActionResult response = sut.Config(); + + // Assert + logger.Received().Log(LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString() == "Invalid Pages Editing Origin"), null, Arg.Any>()); + response.Result.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void ConfigRoute_ValidRequest_OkResponseReturned(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + 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) } })); + PagesSetupController sut = new(pageOptions, logger, renderingEngineOptions); + sut.ControllerContext = new ControllerContext() + { + HttpContext = httpContext + }; + + // Act + 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>()); + 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(); + 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 void RenderRoute_InvalidEditingSecret_ReturnsBadRequestResponse(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + 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 + 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>()); + response.Should().BeOfType(); + } + + [Theory] + [AutoNSubstituteData] + public void RenderRoute_ValidRequest_OkResponseReturned(IOptions pageOptions, ILogger logger, IOptions renderingEngineOptions) + { + // Arrange + string expectedRoute = "test_route"; + string expectedMode = "test_mode"; + Guid expectedItemId = Guid.NewGuid(); + int expectedVersion = 0; + string expectedLanguage = "test_lang"; + string expectedSite = "test_site"; + string expectedLayoutKind = "test_layoutKind"; + string expectedTenantId = "test_tenant_id"; + 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.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.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 + 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>()); + 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}"; + 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 new file mode 100644 index 0000000..06e66cc --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Extensions/PagesAppConfigurationExtensionsFixture.cs @@ -0,0 +1,20 @@ +using FluentAssertions; +using Sitecore.AspNetCore.SDK.Pages.Configuration; +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!, new PagesOptions()); + + // Assert + action.Should().Throw(); + } + } +} \ 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 new file mode 100644 index 0000000..1c67818 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/Constants.cs @@ -0,0 +1,224 @@ +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; + +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 + } + } + }; + } + } + + public static GraphQLResponse EditingLayoutQueryResponseWithDictionaryPaging + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse("{\"test\":\"value\"}").RootElement + } + } + }; + } + } + + public static GraphQLResponse MockEditingLayoutQueryResponse + { + get + { + return new GraphQLResponse + { + Data = new EditingLayoutQueryResponse + { + Item = new ItemModel + { + Rendered = JsonDocument.Parse(@"{ ""sitecore"" : {}}").RootElement + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_Placeholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", [] + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_NestedPlaceholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", [ + new Component + { + Name = "component_1", + Id = "component_1", + Placeholders = new Dictionary + { + { + "nested_placeholder_1", [] + } + } + } + ] + } + } + } + } + }; + } + } + + public static SitecoreLayoutResponseContent MockLayoutResponse_WithComponentInPlaceholder + { + get + { + return new SitecoreLayoutResponseContent + { + Sitecore = new SitecoreData + { + Route = new Route + { + Placeholders = new Dictionary + { + { + "placeholder_1", [ + 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 Component + { + Name = "component_1", + Id = "component_1", + Placeholders = new Dictionary + { + { + "nested_placeholder_1", [ + 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 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 new file mode 100644 index 0000000..5eeda55 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Request/Handlers/GraphQL/GraphQLEditingServiceHandlerFixture.cs @@ -0,0 +1,293 @@ +using System.Diagnostics.CodeAnalysis; +using AutoFixture; +using AutoFixture.Idioms; +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.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.Request.Handlers.GraphQL; +using Sitecore.AspNetCore.SDK.Pages.Services; +using Xunit; + +namespace Sitecore.AspNetCore.SDK.Pages.Tests.Request.Handlers.GraphQL; + +public class GraphQLEditingServiceHandlerFixture +{ + [ExcludeFromCodeCoverage] + public static Action AutoSetup => f => + { + IGraphQLClient client = Substitute.For(); + f.Inject(client); + + ISitecoreLayoutSerializer mockSerializer = Substitute.For(); + f.Inject(mockSerializer); + + IDictionaryService mockDictionaryService = Substitute.For(); + f.Inject(mockDictionaryService); + + 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_lang", "en" + }, + { + "sc_site", "site_1234" + } + }; + f.Inject(request); + }; + + [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() + { + { + "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(IGraphQLClient client, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.SimpleEditingLayoutQueryResponse); + GraphQLEditingServiceHandler sut = new(client, 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(IGraphQLClient client, SitecoreLayoutRequest request, IDictionaryService mockDictionaryService) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.EditingLayoutQueryResponseWithDictionaryPaging); + + GraphQLEditingServiceHandler sut = new(client, 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(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_Placeholder); + GraphQLEditingServiceHandler sut = new(client, 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(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_NestedPlaceholder); + GraphQLEditingServiceHandler sut = new(client, 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("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"); + } + + [Theory] + [AutoNSubstituteData] + public async Task Request_ValidRequest_RenderingChromesAreAdded(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_WithComponentInPlaceholder); + GraphQLEditingServiceHandler sut = new(client, 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(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentInNestedPlaceholder); + GraphQLEditingServiceHandler sut = new(client, 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(IGraphQLClient client, ISitecoreLayoutSerializer mockSerializer, SitecoreLayoutRequest request) + { + // Arrange + client.SendQueryAsync(Arg.Any()).Returns(Constants.MockEditingLayoutQueryResponse); + mockSerializer.Deserialize(Arg.Any()).Returns(Constants.MockLayoutResponse_ComponentWithField); + GraphQLEditingServiceHandler sut = new(client, 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..cd0412c --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/Constants.cs @@ -0,0 +1,78 @@ +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() + { + 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() + { + 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..649cecb --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.Pages.Tests/Services/DictionaryServiceFixture.cs @@ -0,0 +1,124 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +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(); + PropertyInfo? afterProperty = afterVariable.GetProperty("after"); + if (afterProperty == null) + { + return false; + } + + object? afterVariableValue = afterProperty.GetValue(graphQlRequst["variables"]); + if (afterVariableValue == null) + { + return false; + } + + return afterVariableValue.ToString() == expectedAfterValue; + } +} 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 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..6621464 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Controllers/PagesController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Sitecore.AspNetCore.SDK.RenderingEngine.Extensions; +using Sitecore.AspNetCore.SDK.RenderingEngine.Interfaces; + +namespace Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests.Controllers; + +public class PagesController : Controller +{ + public IActionResult Index() + { + 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 new file mode 100644 index 0000000..0878a8a --- /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 + HttpResponseMessage? response = await client.GetAsync(url); + string? 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/PagesSetupRoutingFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/PagesSetupRoutingFixture.cs new file mode 100644 index 0000000..72c3691 --- /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 + HttpResponseMessage? 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 + HttpResponseMessage? 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 + HttpResponseMessage? 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 + HttpResponseMessage? 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 + HttpResponseMessage? 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 + HttpResponseMessage? 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 + HttpResponseMessage? 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..7b65ba8 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/Fixtures/Pages/TestPagesProgram.cs @@ -0,0 +1,44 @@ +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.MapControllerRoute( + name: "default", + pattern: "{controller=Pages}/{action=Index}"); + +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/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.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..64f1474 --- /dev/null +++ b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Integration.Tests/TestWebApplicationFactory.cs @@ -0,0 +1,26 @@ +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 +{ + 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())) + .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/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/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/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(); 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/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/Fields/DateTagHelperFixture.cs index 33c2a2c..1ba472b 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); @@ -337,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 ab8d12e..e9d9baf 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); @@ -711,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 49cb967..e90af83 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); @@ -783,6 +784,33 @@ 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 c577671..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,8 @@ 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; @@ -37,7 +39,7 @@ public class NumberTagHelperFixture return Task.FromResult(tagHelperContent); }); - f.Register(() => new NumberTagHelper()); + f.Register(() => new NumberTagHelper(new EditableChromeRenderer())); f.Inject(tagHelperContext); f.Inject(tagHelperOutput); @@ -301,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..dde4261 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,33 @@ 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 1e7a04e..c0e4619 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); @@ -184,6 +189,33 @@ 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]; diff --git a/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs b/tests/Sitecore.AspNetCore.SDK.RenderingEngine.Tests/TagHelpers/PlaceholderTagHelperFixture.cs index b87fe29..8afcc80 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) } } }; @@ -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] 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 @@ + diff --git a/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs b/tests/data/Sitecore.AspNetCore.SDK.TestData/TestConstants.cs index dd596c0..5a2c509 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 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; @@ -73,10 +77,73 @@ 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 = "{}"; + + 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 Site + { + SiteInfo = new SiteInfo + { + Dictionary = new SiteInfoDictionary + { + Results = + [ + new SiteInfoDictionaryItem() + { + Key = "key1", + Value = "value1" + } + ], + PageInfo = new PageInfo + { + HasNext = false, + EndCursor = string.Empty + } + } + } + } + } + }; + } + } + +#pragma warning disable SA1201 #pragma warning disable SA1401 #pragma warning disable CA2211 public static readonly string TestMultilineFieldValue = $"This is {Environment.NewLine} multiline text"; @@ -84,4 +151,5 @@ public static class TestConstants 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