diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs index d721bc213..c9fb7d43a 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs @@ -23,7 +23,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch { [AbsoluteUrl] [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the elastic search instance or cluster.")] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] [Editor(RuleFieldEditor.Url)] public Uri Host { get; set; } diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 40b716df6..868823d9d 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -76,7 +76,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch { ruleDescription = $"Upsert to index: {action.IndexName}"; - ElasticContent content; + ElasticSearchContent content; try { string jsonString; @@ -91,11 +91,11 @@ namespace Squidex.Extensions.Actions.ElasticSearch jsonString = ToJson(@event); } - content = serializer.Deserialize(jsonString); + content = serializer.Deserialize(jsonString); } catch (Exception ex) { - content = new ElasticContent + content = new ElasticSearchContent { More = new Dictionary { @@ -144,7 +144,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch } } - public sealed class ElasticContent + public sealed class ElasticSearchContent { public string ContentId { get; set; } diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs index 01160ecd8..e22acf0e0 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs @@ -14,7 +14,7 @@ namespace Squidex.Extensions.Actions.OpenSearch { [RuleAction( Title = "OpenSearch", - IconImage = "", + IconImage = "", IconColor = "#005EB8", Display = "Populate OpenSearch index", Description = "Populate a full text search index in OpenSearch.", @@ -23,7 +23,7 @@ namespace Squidex.Extensions.Actions.OpenSearch { [AbsoluteUrl] [LocalizedRequired] - [Display(Name = "Server Url", Description = "The url to the elastic search instance or cluster.")] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] [Editor(RuleFieldEditor.Url)] public Uri Host { get; set; } diff --git a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs index 3c94eeb7b..c43f10509 100644 --- a/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs @@ -76,7 +76,7 @@ namespace Squidex.Extensions.Actions.OpenSearch { ruleDescription = $"Upsert to index: {action.IndexName}"; - ElasticContent content; + OpenSearchContent content; try { string jsonString; @@ -91,11 +91,11 @@ namespace Squidex.Extensions.Actions.OpenSearch jsonString = ToJson(@event); } - content = serializer.Deserialize(jsonString); + content = serializer.Deserialize(jsonString); } catch (Exception ex) { - content = new ElasticContent + content = new OpenSearchContent { More = new Dictionary { @@ -144,7 +144,7 @@ namespace Squidex.Extensions.Actions.OpenSearch } } - public sealed class ElasticContent + public sealed class OpenSearchContent { public string ContentId { get; set; } diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs new file mode 100644 index 000000000..980fc6272 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Extensions.Actions.Typesense +{ + [RuleAction( + Title = "Typesense", + IconImage = "", + IconColor = "#1035bc", + Display = "Populate Typesense index", + Description = "Populate a full text search index in Typesense.", + ReadMore = "https://www.elastic.co/")] + public sealed record TypesenseAction : RuleAction + { + [AbsoluteUrl] + [LocalizedRequired] + [Display(Name = "Server Url", Description = "The url to the instance or cluster.")] + [Editor(RuleFieldEditor.Url)] + public Uri Host { get; set; } + + [LocalizedRequired] + [Display(Name = "Index Name", Description = "The name of the index.")] + [Editor(RuleFieldEditor.Text)] + [Formattable] + public string IndexName { get; set; } + + [LocalizedRequired] + [Display(Name = "Api Key", Description = "The api key.")] + [Editor(RuleFieldEditor.Text)] + public string ApiKey { get; set; } + + [Display(Name = "Document", Description = "The optional custom document.")] + [Editor(RuleFieldEditor.TextArea)] + [Formattable] + public string Document { get; set; } + + [Display(Name = "Deletion", Description = "The condition when to delete the document.")] + [Editor(RuleFieldEditor.Text)] + public string Delete { get; set; } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs new file mode 100644 index 000000000..b93370d1b --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs @@ -0,0 +1,155 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text; +using System.Text.Json.Serialization; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +#pragma warning disable IDE0059 // Value assigned to symbol is never used +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Extensions.Actions.Typesense +{ + public sealed class TypesenseActionHandler : RuleActionHandler + { + private readonly IScriptEngine scriptEngine; + private readonly IHttpClientFactory httpClientFactory; + private readonly IJsonSerializer serializer; + + public TypesenseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, IScriptEngine scriptEngine, IJsonSerializer serializer) + : base(formatter) + { + this.scriptEngine = scriptEngine; + this.httpClientFactory = httpClientFactory; + this.serializer = serializer; + } + + protected override async Task<(string Description, TypesenseJob Data)> CreateJobAsync(EnrichedEvent @event, TypesenseAction action) + { + var delete = @event.ShouldDelete(scriptEngine, action.Delete); + + string contentId; + + if (@event is IEnrichedEntityEvent enrichedEntityEvent) + { + contentId = enrichedEntityEvent.Id.ToString(); + } + else + { + contentId = DomainId.NewGuid().ToString(); + } + + var indexName = await FormatAsync(action.IndexName, @event); + + var ruleDescription = string.Empty; + var ruleJob = new TypesenseJob + { + ServerUrl = $"{action.Host.ToString().TrimEnd('/')}/collections/{indexName}/documents", + ServerKey = action.ApiKey, + ContentId = contentId + }; + + if (delete) + { + ruleDescription = $"Delete entry index: {action.IndexName}"; + } + else + { + ruleDescription = $"Upsert to index: {action.IndexName}"; + + TypesenseContent content; + try + { + string jsonString; + + if (!string.IsNullOrEmpty(action.Document)) + { + jsonString = await FormatAsync(action.Document, @event); + jsonString = jsonString?.Trim(); + } + else + { + jsonString = ToJson(@event); + } + + content = serializer.Deserialize(jsonString); + } + catch (Exception ex) + { + content = new TypesenseContent + { + More = new Dictionary + { + ["error"] = $"Invalid JSON: {ex.Message}" + } + }; + } + + content.Id = contentId; + + ruleJob.Content = serializer.Serialize(content, true); + } + + return (ruleDescription, ruleJob); + } + + protected override async Task ExecuteJobAsync(TypesenseJob job, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(job.ServerUrl)) + { + return Result.Ignored(); + } + + var httpClient = httpClientFactory.CreateClient(); + + HttpRequestMessage request; + + if (job.Content != null) + { + request = new HttpRequestMessage(HttpMethod.Post, $"{job.ServerUrl}?action=upsert") + { + Content = new StringContent(job.Content, Encoding.UTF8, "application/json") + }; + } + else + { + request = new HttpRequestMessage(HttpMethod.Delete, $"{job.ServerUrl}/{job.ContentId}"); + } + + using (request) + { + request.Headers.TryAddWithoutValidation("X-Typesense-Api-Key", job.ServerKey); + + return await httpClient.OneWayRequestAsync(request, job.Content, ct); + } + } + } + + public sealed class TypesenseContent + { + public string Id { get; set; } + + [JsonExtensionData] + public Dictionary More { get; set; } = new Dictionary(); + } + + public sealed class TypesenseJob + { + public string ServerUrl { get; set; } + + public string ServerKey { get; set; } + + public string Content { get; set; } + + public string ContentId { get; set; } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs new file mode 100644 index 000000000..70d051c68 --- /dev/null +++ b/backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.Plugins; + +namespace Squidex.Extensions.Actions.Typesense +{ + public sealed class TypesensePlugin : IPlugin + { + public void ConfigureServices(IServiceCollection services, IConfiguration config) + { + services.AddRuleAction(); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs index 8be80f740..157e5ccf4 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -90,57 +90,53 @@ namespace Squidex.Extensions.Actions.Webhook protected override async Task ExecuteJobAsync(WebhookJob job, CancellationToken ct = default) { - using (var httpClient = httpClientFactory.CreateClient()) + var httpClient = httpClientFactory.CreateClient(); + + var method = HttpMethod.Post; + + switch (job.Method) { - var method = HttpMethod.Post; + case WebhookMethod.PUT: + method = HttpMethod.Put; + break; + case WebhookMethod.GET: + method = HttpMethod.Get; + break; + case WebhookMethod.DELETE: + method = HttpMethod.Delete; + break; + case WebhookMethod.PATCH: + method = HttpMethod.Patch; + break; + } - switch (job.Method) - { - case WebhookMethod.PUT: - method = HttpMethod.Put; - break; - case WebhookMethod.GET: - method = HttpMethod.Get; - break; - case WebhookMethod.DELETE: - method = HttpMethod.Delete; - break; - case WebhookMethod.PATCH: - method = HttpMethod.Patch; - break; - } + using var request = new HttpRequestMessage(method, job.RequestUrl); - var request = new HttpRequestMessage(method, job.RequestUrl); + if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) + { + var mediaType = job.RequestBodyType.Or("application/json"); - if (!string.IsNullOrEmpty(job.RequestBody) && job.Method != WebhookMethod.GET) - { - var mediaType = job.RequestBodyType.Or("application/json"); + request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); + } - request.Content = new StringContent(job.RequestBody, Encoding.UTF8, mediaType); - } + request.Headers.Add("User-Agent", "Squidex Webhook"); - using (request) + if (job.Headers != null) + { + foreach (var (key, value) in job.Headers) { - request.Headers.Add("User-Agent", "Squidex Webhook"); - - if (job.Headers != null) - { - foreach (var (key, value) in job.Headers) - { - request.Headers.TryAddWithoutValidation(key, value); - } - } + request.Headers.TryAddWithoutValidation(key, value); + } + } - if (!string.IsNullOrWhiteSpace(job.RequestSignature)) - { - request.Headers.Add("X-Signature", job.RequestSignature); - } + if (!string.IsNullOrWhiteSpace(job.RequestSignature)) + { + request.Headers.Add("X-Signature", job.RequestSignature); + } - request.Headers.Add("X-Application", "Squidex Webhook"); + request.Headers.Add("X-Application", "Squidex Webhook"); - return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); - } - } + return await httpClient.OneWayRequestAsync(request, job.RequestBody, ct); } } diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index a522c964a..a974d4ea9 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -44,21 +44,26 @@ namespace Squidex.Infrastructure return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; } - public static string BuildFullUrl(this string baseUrl, string path, bool trailingSlash = false) + public static string ToHexString(this byte[] data) { - Guard.NotNull(path); + unchecked + { + var hex = new char[data.Length * 2]; - var url = $"{baseUrl.TrimEnd('/')}/{path.Trim('/')}"; + int n = 0; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; - if (trailingSlash && - url.IndexOf("#", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf("?", StringComparison.OrdinalIgnoreCase) < 0 && - url.IndexOf(";", StringComparison.OrdinalIgnoreCase) < 0) - { - url += "/"; - } + byte b1 = (byte)(b >> 4); + byte b2 = (byte)(b & 0xF); - return url; + hex[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10)); + hex[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10)); + } + + return new string(hex); + } } public static string JoinNonEmpty(string separator, params string?[] parts) diff --git a/backend/src/Squidex.Web/ETagUtils.cs b/backend/src/Squidex.Web/ETagUtils.cs index 3c3bf36ad..867d39f4b 100644 --- a/backend/src/Squidex.Web/ETagUtils.cs +++ b/backend/src/Squidex.Web/ETagUtils.cs @@ -14,6 +14,11 @@ namespace Squidex.Web return $"W/{etag}"; } + public static bool IsStrongEtag(string etag) + { + return !IsWeakEtag(etag.AsSpan()); + } + public static bool IsWeakEtag(string etag) { return IsWeakEtag(etag.AsSpan()); diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index 51c251598..dff1b4776 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -31,6 +31,8 @@ namespace Squidex.Web.Pipeline var resultContext = await next(); + cachingManager.Finish(httpContext); + if (httpContext.Response.HasStarted == false && httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && IsCacheable(httpContext, etag)) diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index 2266fabe3..fdd43319a 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -34,6 +34,7 @@ namespace Squidex.Web.Pipeline private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); private readonly int maxKeysSize; private bool hasDependency; + private bool isFinished; public CacheContext(int maxKeysSize) { @@ -49,12 +50,11 @@ namespace Squidex.Web.Pipeline public void AddDependency(string key, long version) { - if (key != default) + if (!string.IsNullOrWhiteSpace(key)) { + slimLock.EnterWriteLock(); try { - slimLock.EnterWriteLock(); - keys.Add(key); hasher.AppendData(Encoding.Default.GetBytes(key)); @@ -71,18 +71,16 @@ namespace Squidex.Web.Pipeline public void AddDependency(T value) { - if (value is not null) + var formatted = value?.ToString(); + + if (formatted != null) { + slimLock.EnterWriteLock(); try { - slimLock.EnterWriteLock(); - - var formatted = value.ToString(); + hasher.AppendData(Encoding.Default.GetBytes(formatted)); - if (formatted != null) - { - hasher.AppendData(Encoding.Default.GetBytes(formatted)); - } + hasDependency = true; } finally { @@ -93,12 +91,21 @@ namespace Squidex.Web.Pipeline public void Finish(HttpResponse response, ObjectPool stringBuilderPool) { + // Finish might be called multiple times. + if (isFinished) + { + return; + } + + // Set to finish before we start to ensure that we do not call it again in case of an error. + isFinished = true; + if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) { using (Telemetry.Activities.StartActivity("CalculateEtag")) { var cacheBuffer = hasher.GetHashAndReset(); - var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant(); + var cacheString = cacheBuffer.ToHexString(); response.Headers.Add(HeaderNames.ETag, cacheString); } @@ -185,20 +192,48 @@ namespace Squidex.Web.Pipeline }); } + public void Reset(HttpContext httpContext) + { + Guard.NotNull(httpContext); + + httpContext.Features.Set(null); + } + public void Start(HttpContext httpContext) { Guard.NotNull(httpContext); var maxKeysSize = GetKeysSize(httpContext); + // Ensure that we only add the cache context once. + if (httpContext.Features.Get() != null) + { + return; + } + httpContext.Features.Set(new CacheContext(maxKeysSize)); } + public void Finish(HttpContext httpContext) + { + Guard.NotNull(httpContext); + + var cacheContext = httpContext.Features.Get(); + + // If the cache context has not been set it does not make sense to handle it now. + if (cacheContext == null) + { + return; + } + + cacheContext.Finish(httpContext.Response, stringBuilderPool); + } + private int GetKeysSize(HttpContext httpContext) { var headers = httpContext.Request.Headers; - if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || !int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size)) + if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || TryParseHeader(header, out var size)) { size = cachingOptions.MaxSurrogateKeysSize; } @@ -206,10 +241,16 @@ namespace Squidex.Web.Pipeline return Math.Min(MaxAllowedKeysSize, size); } + private static bool TryParseHeader(StringValues header, out int size) + { + return !int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out size); + } + public void AddDependency(DomainId key, long version) { var cacheContext = httpContextAccessor.HttpContext?.Features.Get(); + // The cache context can be null if start has never been called. cacheContext?.AddDependency(key.ToString(), version); } @@ -217,6 +258,7 @@ namespace Squidex.Web.Pipeline { var cacheContext = httpContextAccessor.HttpContext?.Features.Get(); + // The cache context can be null if start has never been called. cacheContext?.AddDependency(value); } @@ -231,6 +273,7 @@ namespace Squidex.Web.Pipeline var cacheContext = httpContext.Features.Get(); + // The cache context can be null if start has never been called. if (cacheContext == null) { return; @@ -240,14 +283,5 @@ namespace Squidex.Web.Pipeline cacheContext?.AddHeader(header, value); } - - public void Finish(HttpContext httpContext) - { - Guard.NotNull(httpContext); - - var cacheContext = httpContext.Features.Get(); - - cacheContext?.Finish(httpContext.Response, stringBuilderPool); - } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs index fc3d5eb47..ce042dbcd 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs @@ -40,28 +40,6 @@ namespace Squidex.Infrastructure Assert.Equal(value, value.Or("fallback")); } - [Theory] - [InlineData("http://squidex.io/base/", "path/to/res", false, "http://squidex.io/base/path/to/res")] - [InlineData("http://squidex.io/base/", "path/to/res", true, "http://squidex.io/base/path/to/res/")] - [InlineData("http://squidex.io/base/", "/path/to/res", true, "http://squidex.io/base/path/to/res/")] - public void Should_provide_full_url_without_query_or_fragment(string baseUrl, string path, bool trailingSlash, string output) - { - var actual = baseUrl.BuildFullUrl(path, trailingSlash); - - Assert.Equal(output, actual); - } - - [Theory] - [InlineData("http://squidex.io/base/", "path/to/res?query=1", false, "http://squidex.io/base/path/to/res?query=1")] - [InlineData("http://squidex.io/base/", "path/to/res#query=1", true, "http://squidex.io/base/path/to/res#query=1")] - [InlineData("http://squidex.io/base/", "path/to/res;query=1", true, "http://squidex.io/base/path/to/res;query=1")] - public void Should_provide_full_url_wit_query_or_fragment(string baseUrl, string path, bool trailingSlash, string output) - { - var actual = baseUrl.BuildFullUrl(path, trailingSlash); - - Assert.Equal(output, actual); - } - [Fact] public void Should_join_non_empty_if_all_are_valid() { @@ -101,5 +79,21 @@ namespace Squidex.Infrastructure Assert.Equal("Hello \\\"World\\\"", actual); } + + [Fact] + public void Should_calculate_hex_code_from_empty_array() + { + var actual = Array.Empty().ToHexString(); + + Assert.Equal(string.Empty, actual); + } + + [Fact] + public void Should_calculate_hex_code_from_byte_array() + { + var actual = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C }.ToHexString(); + + Assert.Equal("0001FF1A2B3C", actual); + } } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs index 5d1c93231..f4b0b2d9f 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs @@ -63,6 +63,22 @@ namespace Squidex.Web.Pipeline Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); } + [Fact] + public async Task Should_return_304_for_same_etags_from_cache_manager() + { + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "2C70E12B7A0646F92279F427C7B38E7334D8E5389CFF167A1DC30E73F826B683"; + + await sut.OnActionExecutionAsync(executingContext, () => + { + cachingManager.AddDependency("key"); + + return Task.FromResult(executedContext); + }); + + Assert.Equal(304, ((StatusCodeResult)executedContext.Result!).StatusCode); + } + [Fact] public async Task Should_not_return_304_for_different_etags() { diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs index 0fecfed6c..ad067fd9b 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs @@ -320,7 +320,7 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_not_add_header_to_etag_if_not_found() + public async Task Should_not_add_header_to_etag_if_not_found_in_request() { var id1 = DomainId.NewGuid(); @@ -360,7 +360,7 @@ namespace Squidex.Web.Pipeline var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - Assert.StartsWith("W/", etag, StringComparison.Ordinal); + Assert.True(ETagUtils.IsWeakEtag(etag)); Assert.True(etag.Length > 20); } @@ -381,7 +381,7 @@ namespace Squidex.Web.Pipeline var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); - Assert.False(etag.StartsWith("W/", StringComparison.Ordinal)); + Assert.True(ETagUtils.IsStrongEtag(etag)); Assert.True(etag.Length > 20); } @@ -405,6 +405,8 @@ namespace Squidex.Web.Pipeline private async Task MakeRequestAsync(Action? action = null) { + cachingManager.Reset(httpContext); + await sut.InvokeAsync(httpContext); action?.Invoke(); diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index 4fc8fdf6e..7436b321b 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -165,13 +165,13 @@ namespace TestSuite.ApiTests public async Task Should_not_create_very_big_asset() { // STEP 1: Create small asset - await _.Assets.UploadFileAsync(_.AppName, 1_000_000); + await _.Assets.UploadRandomFileAsync(_.AppName, 1_000_000); // STEP 2: Create big asset var ex = await Assert.ThrowsAnyAsync(() => { - return _.Assets.UploadFileAsync(_.AppName, 10_000_000); + return _.Assets.UploadRandomFileAsync(_.AppName, 10_000_000); }); // Client library cannot catch this exception properly. diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs index c57ef199d..8c3500a5a 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs @@ -31,7 +31,7 @@ namespace TestSuite.ApiTests public async Task Should_backup_and_restore_app() { // Load the backup from another URL, because the public URL is might not be accessible for the server. - var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.ServerUrl); + var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); var appNameRestore = $"{appName}-restore"; @@ -80,7 +80,7 @@ namespace TestSuite.ApiTests public async Task Should_backup_and_restore_app_with_deleted_app() { // Load the backup from another URL, because the public URL is might not be accessible for the server. - var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.ServerUrl); + var backupUrl = TestHelpers.GetAndPrintValue("config:backupUrl", _.Url); // STEP 1: Create app var createRequest = new CreateAppDto diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs index e6f65d8e6..81938be47 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs @@ -12,11 +12,11 @@ using TestSuite.Model; namespace TestSuite.ApiTests { - public class CDNTests : IClassFixture + public class CDNTests : IClassFixture { - public CloudFixture _ { get; } + public ClientCloudFixture _ { get; } - public CDNTests(CloudFixture fixture) + public CDNTests(ClientCloudFixture fixture) { _ = fixture; } diff --git a/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs b/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs index 45310792c..33ef50856 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Security.Cryptography; -using System.Text; using Squidex.ClientLibrary; using Squidex.ClientLibrary.Management; using TestSuite.Fixtures; @@ -42,8 +40,7 @@ namespace TestSuite return false; } - public static async Task> WaitForContentAsync(this IContentsClient contentsClient, ContentQuery q, - Func predicate, TimeSpan timeout) where TEntity : Content where TData : class, new() + public static async Task> WaitForContentAsync(this IContentsClient contentsClient, ContentQuery q, Func predicate, TimeSpan timeout) where TEntity : Content where TData : class, new() { try { @@ -68,8 +65,7 @@ namespace TestSuite return new ContentsResult(); } - public static async Task> WaitForSearchAsync(this ISearchClient searchClient, string app, string query, - Func predicate, TimeSpan timeout) + public static async Task> WaitForSearchAsync(this ISearchClient searchClient, string app, string query, Func predicate, TimeSpan timeout) { try { @@ -94,8 +90,7 @@ namespace TestSuite return new List(); } - public static async Task> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel, - Func predicate, TimeSpan timeout) + public static async Task> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel, Func predicate, TimeSpan timeout) { try { @@ -120,8 +115,7 @@ namespace TestSuite return new List(); } - public static async Task> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id, - TimeSpan timeout) + public static async Task> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id, TimeSpan timeout) { try { @@ -146,8 +140,7 @@ namespace TestSuite return await assetsClient.GetTagsAsync(app); } - public static async Task> WaitForBackupsAsync(this IBackupsClient backupsClient, string app, - Func predicate, TimeSpan timeout) + public static async Task> WaitForBackupsAsync(this IBackupsClient backupsClient, string app, Func predicate, TimeSpan timeout) { try { @@ -172,8 +165,7 @@ namespace TestSuite return null; } - public static async Task WaitForRestoreAsync(this IBackupsClient backupsClient, - Func predicate, TimeSpan timeout) + public static async Task WaitForRestoreAsync(this IBackupsClient backupsClient, Func predicate, TimeSpan timeout) { try { @@ -198,13 +190,13 @@ namespace TestSuite return null; } - public static async Task DownloadAsync(this ClientManagerFixture fixture, AssetDto asset, int? version = null) + public static async Task DownloadAsync(this ClientFixture fixture, AssetDto asset, int? version = null) { var temp = new MemoryStream(); - using (var client = new HttpClient()) + using (var httpClient = new HttpClient()) { - client.BaseAddress = new Uri(fixture.ServerUrl); + httpClient.BaseAddress = new Uri(fixture.Url); var url = asset._links["content"].Href[1..]; @@ -213,21 +205,21 @@ namespace TestSuite url += $"?version={version}"; } - var response = await client.GetAsync(url); - - response.EnsureSuccessStatusCode(); - - await using (var stream = await response.Content.ReadAsStreamAsync()) + using (var response = await httpClient.GetAsync(url)) { - await stream.CopyToAsync(temp); + response.EnsureSuccessStatusCode(); + + await using (var stream = await response.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(temp); + } } } return temp; } - public static async Task UploadFileAsync(this IAssetsClient assetsClients, string app, string path, - AssetDto asset, string fileName = null) + public static async Task UploadFileAsync(this IAssetsClient assetsClients, string app, string path, AssetDto asset, string fileName = null) { var fileInfo = new FileInfo(path); @@ -239,25 +231,23 @@ namespace TestSuite } } - public static async Task UploadFileAsync(this IAssetsClient assetsClients, string app, string path, - string mimeType, string fileName = null, string parentId = null, string id = null) + public static async Task UploadFileAsync(this IAssetsClient assetsClients, string app, string path, string fileType, string fileName = null, string parentId = null, string id = null) { var fileInfo = new FileInfo(path); await using (var stream = fileInfo.OpenRead()) { - var upload = new FileParameter(stream, fileName ?? fileInfo.Name, mimeType); + var upload = new FileParameter(stream, fileName ?? fileInfo.Name, fileType); return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); } } - public static async Task UploadFileAsync(this IAssetsClient assetsClients, string app, int size, - string fileName = null, string parentId = null, string id = null) + public static async Task UploadRandomFileAsync(this IAssetsClient assetsClients, string app, int size, string parentId = null, string id = null) { using (var stream = RandomAsset(size)) { - var upload = new FileParameter(stream, fileName ?? RandomName(".txt"), "text/csv"); + var upload = new FileParameter(stream, RandomName(".txt"), "text/csv"); return await assetsClients.PostAssetAsync(app, parentId, id, true, upload); } diff --git a/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs b/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs index 533478627..fef2d9412 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs @@ -6,189 +6,39 @@ // ========================================================================== using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Squidex.ClientLibrary; -using Squidex.ClientLibrary.Configuration; -using Squidex.ClientLibrary.Management; using TestSuite.Utils; namespace TestSuite { public sealed class ClientManagerWrapper { - private readonly Lazy apps; - private readonly Lazy assets; - private readonly Lazy comments; - private readonly Lazy backups; - private readonly Lazy diagnostics; - private readonly Lazy history; - private readonly Lazy languages; - private readonly Lazy ping; - private readonly Lazy plans; - private readonly Lazy rules; - private readonly Lazy schemas; - private readonly Lazy search; - private readonly Lazy templates; - private readonly Lazy translations; - - public SquidexClientManager ClientManager { get; } - - public IAppsClient Apps - { - get => apps.Value; - } - - public IAssetsClient Assets - { - get => assets.Value; - } - - public IBackupsClient Backups - { - get => backups.Value; - } - - public ICommentsClient Comments - { - get => comments.Value; - } - - public IDiagnosticsClient Diagnostics - { - get => diagnostics.Value; - } - - public IHistoryClient History - { - get => history.Value; - } - - public ILanguagesClient Languages - { - get => languages.Value; - } - - public IPingClient Ping - { - get => ping.Value; - } - - public IPlansClient Plans - { - get => plans.Value; - } - - public IRulesClient Rules - { - get => rules.Value; - } - - public ISchemasClient Schemas - { - get => schemas.Value; - } - - public ISearchClient Search - { - get => search.Value; - } - - public ITemplatesClient Templates - { - get => templates.Value; - } - - public ITranslationsClient Translations - { - get => translations.Value; - } + public ISquidexClientManager ClientManager { get; } public ClientManagerWrapper() { - var appName = TestHelpers.GetAndPrintValue("config:app:name", "integration-tests"); - var clientId = TestHelpers.GetAndPrintValue("config:client:id", "root"); - var clientSecret = TestHelpers.GetAndPrintValue("config:client:secret", "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="); - var serverUrl = TestHelpers.GetAndPrintValue("config:server:url", "https://localhost:5001"); - - ClientManager = new SquidexClientManager(new SquidexOptions - { - AppName = appName, - ClientId = clientId, - ClientSecret = clientSecret, - ClientFactory = null, - Configurator = AcceptAllCertificatesConfigurator.Instance, - ReadResponseAsString = true, - Url = serverUrl - }); - - apps = new Lazy(() => - { - return ClientManager.CreateAppsClient(); - }); - - assets = new Lazy(() => - { - return ClientManager.CreateAssetsClient(); - }); - - backups = new Lazy(() => - { - return ClientManager.CreateBackupsClient(); - }); - - comments = new Lazy(() => - { - return ClientManager.CreateCommentsClient(); - }); - - diagnostics = new Lazy(() => - { - return ClientManager.CreateDiagnosticsClient(); - }); - - history = new Lazy(() => - { - return ClientManager.CreateHistoryClient(); - }); - - languages = new Lazy(() => - { - return ClientManager.CreateLanguagesClient(); - }); - - ping = new Lazy(() => - { - return ClientManager.CreatePingClient(); - }); - - plans = new Lazy(() => - { - return ClientManager.CreatePlansClient(); - }); - - rules = new Lazy(() => - { - return ClientManager.CreateRulesClient(); - }); - - schemas = new Lazy(() => - { - return ClientManager.CreateSchemasClient(); - }); - - search = new Lazy(() => - { - return ClientManager.CreateSearchClient(); - }); - - templates = new Lazy(() => - { - return ClientManager.CreateTemplatesClient(); - }); + var services = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = TestHelpers.GetAndPrintValue("config:app:name", "integration-tests"); + options.ClientId = TestHelpers.GetAndPrintValue("config:client:id", "root"); + options.ClientSecret = TestHelpers.GetAndPrintValue("config:client:secret", "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="); + options.Url = TestHelpers.GetAndPrintValue("config:server:url", "https://localhost:5001"); + options.ReadResponseAsString = true; + }) + .AddSquidexHttpClient() + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + }).Services + .BuildServiceProvider(); - translations = new Lazy(() => - { - return ClientManager.CreateTranslationsClient(); - }); + ClientManager = services.GetRequiredService(); } public async Task ConnectAsync() diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs new file mode 100644 index 000000000..377a942db --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs @@ -0,0 +1,47 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.ClientLibrary; + +namespace TestSuite.Fixtures +{ + public sealed class ClientCloudFixture + { + public ISquidexClientManager ClientManager { get; private set; } + + public ISquidexClientManager CDNClientManager { get; private set; } + + public ClientCloudFixture() + { + ClientManager = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = "squidex-website"; + options.ClientId = "squidex-website:reader"; + options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; + options.ReadResponseAsString = true; + }) + .BuildServiceProvider() + .GetRequiredService(); + + CDNClientManager = + new ServiceCollection() + .AddSquidexClient(options => + { + options.AppName = "squidex-website"; + options.AssetCDN = "https://assets.squidex.io"; + options.ClientId = "squidex-website:reader"; + options.ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x"; + options.ContentCDN = "https://contents.squidex.io"; + }) + .BuildServiceProvider() + .GetRequiredService(); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs index 8c061f2ea..8d7c23e48 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs @@ -5,38 +5,125 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.ClientLibrary; using Squidex.ClientLibrary.Management; +using Xunit; namespace TestSuite.Fixtures { - public class ClientFixture : ClientManagerFixture + public class ClientFixture : IAsyncLifetime { - public IAppsClient Apps => Squidex.Apps; + public ClientManagerWrapper Squidex { get; private set; } - public IAssetsClient Assets => Squidex.Assets; + public string AppName => ClientManager.Options.AppName; - public IBackupsClient Backups => Squidex.Backups; + public string ClientId => ClientManager.Options.ClientId; - public ICommentsClient Comments => Squidex.Comments; + public string ClientSecret => ClientManager.Options.ClientSecret; - public IDiagnosticsClient Diagnostics => Squidex.Diagnostics; + public string Url => ClientManager.Options.Url; - public IHistoryClient History => Squidex.History; + public ISquidexClientManager ClientManager => Squidex.ClientManager; - public ILanguagesClient Languages => Squidex.Languages; + public IAppsClient Apps + { + get => ClientManager.CreateAppsClient(); + } - public IPingClient Ping => Squidex.Ping; + public IAssetsClient Assets + { + get => ClientManager.CreateAssetsClient(); + } - public IPlansClient Plans => Squidex.Plans; + public IBackupsClient Backups + { + get => ClientManager.CreateBackupsClient(); + } - public IRulesClient Rules => Squidex.Rules; + public ICommentsClient Comments + { + get => ClientManager.CreateCommentsClient(); + } - public ISchemasClient Schemas => Squidex.Schemas; + public IDiagnosticsClient Diagnostics + { + get => ClientManager.CreateDiagnosticsClient(); + } - public ISearchClient Search => Squidex.Search; + public IHistoryClient History + { + get => ClientManager.CreateHistoryClient(); + } - public ITemplatesClient Templates => Squidex.Templates; + public ILanguagesClient Languages + { + get => ClientManager.CreateLanguagesClient(); + } - public ITranslationsClient Translations => Squidex.Translations; + public IPingClient Ping + { + get => ClientManager.CreatePingClient(); + } + + public IPlansClient Plans + { + get => ClientManager.CreatePlansClient(); + } + + public IRulesClient Rules + { + get => ClientManager.CreateRulesClient(); + } + + public ISchemasClient Schemas + { + get => ClientManager.CreateSchemasClient(); + } + + public ISearchClient Search + { + get => ClientManager.CreateSearchClient(); + } + + public ITemplatesClient Templates + { + get => ClientManager.CreateTemplatesClient(); + } + + public ITranslationsClient Translations + { + get => ClientManager.CreateTranslationsClient(); + } + + static ClientFixture() + { + VerifierSettings.IgnoreMember("AppName"); + VerifierSettings.IgnoreMember("Created"); + VerifierSettings.IgnoreMember("CreatedBy"); + VerifierSettings.IgnoreMember("EditToken"); + VerifierSettings.IgnoreMember("Href"); + VerifierSettings.IgnoreMember("LastModified"); + VerifierSettings.IgnoreMember("LastModifiedBy"); + VerifierSettings.IgnoreMember("RoleProperties"); + VerifierSettings.IgnoreMember("SchemaName"); + VerifierSettings.IgnoreMembersWithType(); + } + + public virtual async Task InitializeAsync() + { + Squidex = await Factories.CreateAsync(nameof(ClientManagerWrapper), async () => + { + var clientManager = new ClientManagerWrapper(); + + await clientManager.ConnectAsync(); + + return clientManager; + }); + } + + public virtual Task DisposeAsync() + { + return Task.CompletedTask; + } } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs deleted file mode 100644 index dc6ec78e2..000000000 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.ClientLibrary; -using Xunit; - -namespace TestSuite.Fixtures -{ - public class ClientManagerFixture : IAsyncLifetime - { - public ClientManagerWrapper Squidex { get; private set; } - - public string AppName => ClientManager.Options.AppName; - - public string ClientId => ClientManager.Options.ClientId; - - public string ClientSecret => ClientManager.Options.ClientSecret; - - public string ServerUrl => ClientManager.Options.Url; - - public SquidexClientManager ClientManager => Squidex.ClientManager; - - static ClientManagerFixture() - { - VerifierSettings.IgnoreMember("AppName"); - VerifierSettings.IgnoreMember("Created"); - VerifierSettings.IgnoreMember("CreatedBy"); - VerifierSettings.IgnoreMember("EditToken"); - VerifierSettings.IgnoreMember("Href"); - VerifierSettings.IgnoreMember("LastModified"); - VerifierSettings.IgnoreMember("LastModifiedBy"); - VerifierSettings.IgnoreMember("RoleProperties"); - VerifierSettings.IgnoreMember("SchemaName"); - VerifierSettings.IgnoreMembersWithType(); - } - - public virtual async Task InitializeAsync() - { - Squidex = await Factories.CreateAsync(nameof(ClientManagerWrapper), async () => - { - var clientManager = new ClientManagerWrapper(); - - await clientManager.ConnectAsync(); - - return clientManager; - }); - } - - public virtual Task DisposeAsync() - { - return Task.CompletedTask; - } - } -} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CloudFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CloudFixture.cs deleted file mode 100644 index fe135f7e4..000000000 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CloudFixture.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.ClientLibrary; - -namespace TestSuite.Fixtures -{ - public sealed class CloudFixture - { - public SquidexClientManager ClientManager { get; private set; } - - public SquidexClientManager CDNClientManager { get; private set; } - - public CloudFixture() - { - ClientManager = new SquidexClientManager( - new SquidexOptions - { - AppName = "squidex-website", - ClientId = "squidex-website:reader", - ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x" - }); - - CDNClientManager = new SquidexClientManager( - new SquidexOptions - { - AppName = "squidex-website", - AssetCDN = "https://assets.squidex.io", - ClientId = "squidex-website:reader", - ClientSecret = "yy9x4dcxsnp1s34r2z19t88wedbzxn1tfq7uzmoxf60x", - ContentCDN = "https://contents.squidex.io", - }); - } - } -} diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index cb82770c0..fed01e6d1 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -16,7 +16,8 @@ - + + diff --git a/frontend/src/app/features/rules/shared/actions/formattable-input.component.html b/frontend/src/app/features/rules/shared/actions/formattable-input.component.html index f3955be23..600ba1c47 100644 --- a/frontend/src/app/features/rules/shared/actions/formattable-input.component.html +++ b/frontend/src/app/features/rules/shared/actions/formattable-input.component.html @@ -1,4 +1,4 @@ - +
@@ -10,7 +10,7 @@
- +