Browse Source

Update client library. (#927)

* Update client library.

* Fix tests

* Typesense and rules improvements.

* Fix Etag.
pull/929/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
cc9753c37c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  2. 8
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  3. 4
      backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs
  4. 8
      backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchActionHandler.cs
  5. 50
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseAction.cs
  6. 155
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesenseActionHandler.cs
  7. 21
      backend/extensions/Squidex.Extensions/Actions/Typesense/TypesensePlugin.cs
  8. 78
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  9. 27
      backend/src/Squidex.Infrastructure/StringExtensions.cs
  10. 5
      backend/src/Squidex.Web/ETagUtils.cs
  11. 2
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  12. 78
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  13. 38
      backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs
  14. 16
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  15. 8
      backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs
  16. 4
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  17. 4
      backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs
  18. 6
      backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs
  19. 52
      backend/tools/TestSuite/TestSuite.Shared/ClientExtensions.cs
  20. 194
      backend/tools/TestSuite/TestSuite.Shared/ClientManagerWrapper.cs
  21. 47
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientCloudFixture.cs
  22. 117
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs
  23. 58
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs
  24. 39
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/CloudFixture.cs
  25. 3
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj
  26. 4
      frontend/src/app/features/rules/shared/actions/formattable-input.component.html
  27. 3
      frontend/src/app/features/rules/shared/actions/formattable-input.component.ts
  28. 14
      frontend/src/app/features/rules/shared/actions/generic-action.component.html

2
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; }

8
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<ElasticContent>(jsonString);
content = serializer.Deserialize<ElasticSearchContent>(jsonString);
}
catch (Exception ex)
{
content = new ElasticContent
content = new ElasticSearchContent
{
More = new Dictionary<string, object>
{
@ -144,7 +144,7 @@ namespace Squidex.Extensions.Actions.ElasticSearch
}
}
public sealed class ElasticContent
public sealed class ElasticSearchContent
{
public string ContentId { get; set; }

4
backend/extensions/Squidex.Extensions/Actions/OpenSearch/OpenSearchAction.cs

@ -14,7 +14,7 @@ namespace Squidex.Extensions.Actions.OpenSearch
{
[RuleAction(
Title = "OpenSearch",
IconImage = "<svg viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M61.737 23.5a2.263 2.263 0 0 0-2.262 2.263c0 18.618-15.094 33.712-33.712 33.712a2.263 2.263 0 1 0 0 4.525C46.88 64 64 46.88 64 25.763a2.263 2.263 0 0 0-2.263-2.263Z' fill='#fff'/><path d='M48.081 38c2.176-3.55 4.28-8.282 3.866-14.908C51.09 9.367 38.66-1.045 26.921.084c-4.596.441-9.314 4.187-8.895 10.896.182 2.916 1.61 4.637 3.928 5.96 2.208 1.26 5.044 2.057 8.259 2.961 3.883 1.092 8.388 2.32 11.85 4.87 4.15 3.058 6.986 6.603 6.018 13.229Z' fill='#fff'/><path d='M3.919 14C1.743 17.55-.361 22.282.052 28.908.91 42.633 13.342 53.045 25.08 51.916c4.596-.441 9.314-4.187 8.895-10.896-.182-2.916-1.61-4.637-3.928-5.96-2.208-1.26-5.044-2.057-8.259-2.961-3.883-1.092-8.388-2.32-11.85-4.87C5.787 24.17 2.95 20.625 3.919 14Z' fill='#fff'/></svg>",
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><path d='M61.737 23.5a2.263 2.263 0 0 0-2.262 2.263c0 18.618-15.094 33.712-33.712 33.712a2.263 2.263 0 1 0 0 4.525C46.88 64 64 46.88 64 25.763a2.263 2.263 0 0 0-2.263-2.263Z' fill='#fff'/><path d='M48.081 38c2.176-3.55 4.28-8.282 3.866-14.908C51.09 9.367 38.66-1.045 26.921.084c-4.596.441-9.314 4.187-8.895 10.896.182 2.916 1.61 4.637 3.928 5.96 2.208 1.26 5.044 2.057 8.259 2.961 3.883 1.092 8.388 2.32 11.85 4.87 4.15 3.058 6.986 6.603 6.018 13.229Z' fill='#fff'/><path d='M3.919 14C1.743 17.55-.361 22.282.052 28.908.91 42.633 13.342 53.045 25.08 51.916c4.596-.441 9.314-4.187 8.895-10.896-.182-2.916-1.61-4.637-3.928-5.96-2.208-1.26-5.044-2.057-8.259-2.961-3.883-1.092-8.388-2.32-11.85-4.87C5.787 24.17 2.95 20.625 3.919 14Z' fill='#fff'/></svg>",
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; }

8
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<ElasticContent>(jsonString);
content = serializer.Deserialize<OpenSearchContent>(jsonString);
}
catch (Exception ex)
{
content = new ElasticContent
content = new OpenSearchContent
{
More = new Dictionary<string, object>
{
@ -144,7 +144,7 @@ namespace Squidex.Extensions.Actions.OpenSearch
}
}
public sealed class ElasticContent
public sealed class OpenSearchContent
{
public string ContentId { get; set; }

50
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 49.293 50.853'><path d='M15.074 15.493a8.19 8.19 0 0 1 .165 1.601c0 .479-.055.994-.165 1.546l-7.013-.055v18.552c0 1.546.718 2.32 2.154 2.32h4.196c.258.625.386 1.25.386 1.877 0 .625-.036 1.012-.11 1.159-1.693.22-3.442.331-5.245.331-3.57 0-5.356-1.527-5.356-4.582V18.585l-3.92.055A7.91 7.91 0 0 1 0 17.094c0-.515.055-1.049.166-1.601l3.92.055V9.751c0-.994.147-1.694.442-2.098.294-.442.865-.663 1.711-.663H7.73l.331.331v8.283z'/><path d='M18.296 40.848c.036-.81.257-1.693.662-2.65.442-.994.94-1.767 1.491-2.32 2.908 1.583 5.466 2.375 7.675 2.375 1.214 0 2.19-.24 2.926-.718.773-.479 1.16-1.123 1.16-1.933 0-1.288-.994-2.319-2.982-3.092l-3.092-1.16c-4.638-1.692-6.957-4.398-6.957-8.116 0-1.325.24-2.503.718-3.533a7.992 7.992 0 0 1 2.098-2.706c.92-.773 2.006-1.362 3.258-1.767 1.251-.405 2.65-.607 4.196-.607.7 0 1.472.055 2.32.165.882.11 1.766.277 2.65.497.883.184 1.73.405 2.54.663s1.508.534 2.097.828c0 .92-.184 1.877-.552 2.871-.368.994-.865 1.73-1.49 2.209-2.909-1.288-5.43-1.933-7.565-1.933-.957 0-1.712.24-2.264.718-.552.442-.828 1.03-.828 1.767 0 1.141.92 2.043 2.761 2.706l3.368 1.214c2.43.847 4.233 2.006 5.411 3.479 1.178 1.472 1.767 3.184 1.767 5.135 0 2.613-.976 4.711-2.927 6.294-1.95 1.546-4.748 2.32-8.392 2.32-3.57 0-6.92-.903-10.049-2.706z' style='fill:#fffff;fill-opacity:1' transform='translate(0 -.354)'/><path d='M45.373 50.687V.166A9.626 9.626 0 0 1 47.25 0c.736 0 1.417.055 2.042.166v50.521a11.8 11.8 0 0 1-2.042.166c-.7 0-1.326-.056-1.878-.166z'/></svg>",
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; }
}
}

155
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<TypesenseAction, TypesenseJob>
{
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<TypesenseContent>(jsonString);
}
catch (Exception ex)
{
content = new TypesenseContent
{
More = new Dictionary<string, object>
{
["error"] = $"Invalid JSON: {ex.Message}"
}
};
}
content.Id = contentId;
ruleJob.Content = serializer.Serialize(content, true);
}
return (ruleDescription, ruleJob);
}
protected override async Task<Result> 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<string, object> More { get; set; } = new Dictionary<string, object>();
}
public sealed class TypesenseJob
{
public string ServerUrl { get; set; }
public string ServerKey { get; set; }
public string Content { get; set; }
public string ContentId { get; set; }
}
}

21
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<TypesenseAction, TypesenseActionHandler>();
}
}
}

78
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -90,57 +90,53 @@ namespace Squidex.Extensions.Actions.Webhook
protected override async Task<Result> 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);
}
}

27
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)

5
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());

2
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))

78
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>(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<StringBuilder> 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<CacheContext>(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<CacheContext>() != null)
{
return;
}
httpContext.Features.Set(new CacheContext(maxKeysSize));
}
public void Finish(HttpContext httpContext)
{
Guard.NotNull(httpContext);
var cacheContext = httpContext.Features.Get<CacheContext>();
// 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<CacheContext>();
// 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<CacheContext>();
// 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<CacheContext>();
// 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>();
cacheContext?.Finish(httpContext.Response, stringBuilderPool);
}
}
}

38
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<byte>().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);
}
}
}

16
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()
{

8
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();

4
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<Exception>(() =>
{
return _.Assets.UploadFileAsync(_.AppName, 10_000_000);
return _.Assets.UploadRandomFileAsync(_.AppName, 10_000_000);
});
// Client library cannot catch this exception properly.

4
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

6
backend/tools/TestSuite/TestSuite.ApiTests/CDNTests.cs

@ -12,11 +12,11 @@ using TestSuite.Model;
namespace TestSuite.ApiTests
{
public class CDNTests : IClassFixture<CloudFixture>
public class CDNTests : IClassFixture<ClientCloudFixture>
{
public CloudFixture _ { get; }
public ClientCloudFixture _ { get; }
public CDNTests(CloudFixture fixture)
public CDNTests(ClientCloudFixture fixture)
{
_ = fixture;
}

52
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<ContentsResult<TEntity, TData>> WaitForContentAsync<TEntity, TData>(this IContentsClient<TEntity, TData> contentsClient, ContentQuery q,
Func<TEntity, bool> predicate, TimeSpan timeout) where TEntity : Content<TData> where TData : class, new()
public static async Task<ContentsResult<TEntity, TData>> WaitForContentAsync<TEntity, TData>(this IContentsClient<TEntity, TData> contentsClient, ContentQuery q, Func<TEntity, bool> predicate, TimeSpan timeout) where TEntity : Content<TData> where TData : class, new()
{
try
{
@ -68,8 +65,7 @@ namespace TestSuite
return new ContentsResult<TEntity, TData>();
}
public static async Task<IList<SearchResultDto>> WaitForSearchAsync(this ISearchClient searchClient, string app, string query,
Func<SearchResultDto, bool> predicate, TimeSpan timeout)
public static async Task<IList<SearchResultDto>> WaitForSearchAsync(this ISearchClient searchClient, string app, string query, Func<SearchResultDto, bool> predicate, TimeSpan timeout)
{
try
{
@ -94,8 +90,7 @@ namespace TestSuite
return new List<SearchResultDto>();
}
public static async Task<IList<HistoryEventDto>> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel,
Func<HistoryEventDto, bool> predicate, TimeSpan timeout)
public static async Task<IList<HistoryEventDto>> WaitForHistoryAsync(this IHistoryClient historyClient, string app, string channel, Func<HistoryEventDto, bool> predicate, TimeSpan timeout)
{
try
{
@ -120,8 +115,7 @@ namespace TestSuite
return new List<HistoryEventDto>();
}
public static async Task<IDictionary<string, int>> WaitForTagsAsync(this IAssetsClient assetsClient, string app, string id,
TimeSpan timeout)
public static async Task<IDictionary<string, int>> 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<IList<BackupJobDto>> WaitForBackupsAsync(this IBackupsClient backupsClient, string app,
Func<BackupJobDto, bool> predicate, TimeSpan timeout)
public static async Task<IList<BackupJobDto>> WaitForBackupsAsync(this IBackupsClient backupsClient, string app, Func<BackupJobDto, bool> predicate, TimeSpan timeout)
{
try
{
@ -172,8 +165,7 @@ namespace TestSuite
return null;
}
public static async Task<RestoreJobDto> WaitForRestoreAsync(this IBackupsClient backupsClient,
Func<RestoreJobDto, bool> predicate, TimeSpan timeout)
public static async Task<RestoreJobDto> WaitForRestoreAsync(this IBackupsClient backupsClient, Func<RestoreJobDto, bool> predicate, TimeSpan timeout)
{
try
{
@ -198,13 +190,13 @@ namespace TestSuite
return null;
}
public static async Task<MemoryStream> DownloadAsync(this ClientManagerFixture fixture, AssetDto asset, int? version = null)
public static async Task<MemoryStream> 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<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path,
AssetDto asset, string fileName = null)
public static async Task<AssetDto> 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<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, string path,
string mimeType, string fileName = null, string parentId = null, string id = null)
public static async Task<AssetDto> 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<AssetDto> UploadFileAsync(this IAssetsClient assetsClients, string app, int size,
string fileName = null, string parentId = null, string id = null)
public static async Task<AssetDto> 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);
}

194
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<IAppsClient> apps;
private readonly Lazy<IAssetsClient> assets;
private readonly Lazy<ICommentsClient> comments;
private readonly Lazy<IBackupsClient> backups;
private readonly Lazy<IDiagnosticsClient> diagnostics;
private readonly Lazy<IHistoryClient> history;
private readonly Lazy<ILanguagesClient> languages;
private readonly Lazy<IPingClient> ping;
private readonly Lazy<IPlansClient> plans;
private readonly Lazy<IRulesClient> rules;
private readonly Lazy<ISchemasClient> schemas;
private readonly Lazy<ISearchClient> search;
private readonly Lazy<ITemplatesClient> templates;
private readonly Lazy<ITranslationsClient> 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<IAppsClient>(() =>
{
return ClientManager.CreateAppsClient();
});
assets = new Lazy<IAssetsClient>(() =>
{
return ClientManager.CreateAssetsClient();
});
backups = new Lazy<IBackupsClient>(() =>
{
return ClientManager.CreateBackupsClient();
});
comments = new Lazy<ICommentsClient>(() =>
{
return ClientManager.CreateCommentsClient();
});
diagnostics = new Lazy<IDiagnosticsClient>(() =>
{
return ClientManager.CreateDiagnosticsClient();
});
history = new Lazy<IHistoryClient>(() =>
{
return ClientManager.CreateHistoryClient();
});
languages = new Lazy<ILanguagesClient>(() =>
{
return ClientManager.CreateLanguagesClient();
});
ping = new Lazy<IPingClient>(() =>
{
return ClientManager.CreatePingClient();
});
plans = new Lazy<IPlansClient>(() =>
{
return ClientManager.CreatePlansClient();
});
rules = new Lazy<IRulesClient>(() =>
{
return ClientManager.CreateRulesClient();
});
schemas = new Lazy<ISchemasClient>(() =>
{
return ClientManager.CreateSchemasClient();
});
search = new Lazy<ISearchClient>(() =>
{
return ClientManager.CreateSearchClient();
});
templates = new Lazy<ITemplatesClient>(() =>
{
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<ITranslationsClient>(() =>
{
return ClientManager.CreateTranslationsClient();
});
ClientManager = services.GetRequiredService<ISquidexClientManager>();
}
public async Task<ClientManagerWrapper> ConnectAsync()

47
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<ISquidexClientManager>();
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<ISquidexClientManager>();
}
}
}

117
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<DateTimeOffset>();
}
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;
}
}
}

58
backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs

@ -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<DateTimeOffset>();
}
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;
}
}
}

39
backend/tools/TestSuite/TestSuite.Shared/Fixtures/CloudFixture.cs

@ -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",
});
}
}
}

3
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -16,7 +16,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="9.2.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="10.0.1" />
<PackageReference Include="Squidex.ClientLibrary.ServiceExtensions" Version="10.0.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="Verify" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />

4
frontend/src/app/features/rules/shared/actions/formattable-input.component.html

@ -1,4 +1,4 @@
<ng-container *ngIf="type === 'Text'; else textarea">
<ng-container *ngIf="type === 'Text'; else code">
<div class="input-group">
<input class="form-control" ngDefaultControl />
@ -10,7 +10,7 @@
</div>
</ng-container>
<ng-template #textarea>
<ng-template #code>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-toggle" *ngFor="let supportedMode of modes" [class.active]="mode === supportedMode" (click)="setMode(supportedMode)" tabindex="-1">
{{supportedMode}}

3
frontend/src/app/features/rules/shared/actions/formattable-input.component.ts

@ -34,9 +34,6 @@ export class FormattableInputComponent implements ControlValueAccessor, AfterVie
@Input()
public type: 'Text' | 'Code' = 'Text';
@Input()
public formattable = true;
@Input()
public completion: ReadonlyArray<{ path: string; description: string }> | undefined | null;

14
frontend/src/app/features/rules/shared/actions/generic-action.component.html

@ -9,10 +9,20 @@
<ng-container [ngSwitch]="property.editor">
<ng-container *ngSwitchCase="'Text'">
<sqx-formattable-input [formControlName]="property.name" type="Text" [formattable]="property.isFormattable"></sqx-formattable-input>
<ng-container *ngIf="property.isFormattable; else plain">
<sqx-formattable-input [formControlName]="property.name" type="Text"></sqx-formattable-input>
</ng-container>
<ng-template #plain>
<input class="form-control" id="{{property.name}}" [formControlName]="property.name">
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'TextArea'">
<sqx-formattable-input [completion]="ruleCompletions | async" [formControlName]="property.name" type="Code" [formattable]="property.isFormattable"></sqx-formattable-input>
<ng-container *ngIf="property.isFormattable; else plain">
<sqx-formattable-input [completion]="ruleCompletions | async" [formControlName]="property.name" type="Code"></sqx-formattable-input>
</ng-container>
<ng-template #plain>
<textarea class="form-control" id="{{property.name}}" [formControlName]="property.name"></textarea>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'Javascript'">
<sqx-code-editor [completion]="ruleCompletions | async" [formControlName]="property.name" [height]="350"></sqx-code-editor>

Loading…
Cancel
Save