Browse Source

More Tests

* More tests and a fix to etag handling.

* Fix content form.

* Tests improved.

* Make username and password optional.

* Simplified tests again.

* Fix API tests
pull/1052/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
c4d79185f2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectActionHandler.cs
  2. 2
      backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectPlugin.cs
  3. 6
      backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs
  4. 1
      backend/i18n/frontend_en.json
  5. 1
      backend/i18n/frontend_fr.json
  6. 1
      backend/i18n/frontend_it.json
  7. 1
      backend/i18n/frontend_nl.json
  8. 1
      backend/i18n/frontend_pt.json
  9. 1
      backend/i18n/frontend_zh.json
  10. 1
      backend/i18n/source/frontend_en.json
  11. 8
      backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs
  12. 8
      backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs
  13. 18
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  14. 6
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  15. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs
  16. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  19. 33
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs
  20. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs
  21. 9
      backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs
  22. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs
  23. 7
      backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotifications.cs
  24. 3
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  25. 2
      backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs
  26. 7
      backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs
  27. 73
      backend/src/Squidex.Infrastructure/HashExtensions.cs
  28. 18
      backend/src/Squidex.Web/BuilderExtensions.cs
  29. 22
      backend/src/Squidex.Web/ETagExtensions.cs
  30. 58
      backend/src/Squidex.Web/ETagUtils.cs
  31. 20
      backend/src/Squidex.Web/Extensions.cs
  32. 14
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  33. 6
      backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs
  34. 2
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  35. 10
      backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs
  36. 30
      backend/src/Squidex/Areas/Frontend/Startup.cs
  37. 7
      backend/src/Squidex/Squidex.csproj
  38. 4
      backend/src/Squidex/Startup.cs
  39. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs
  40. 4
      backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs
  41. 28
      backend/tests/Squidex.Infrastructure.Tests/HashExtensionsTests.cs
  42. 4
      backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs
  43. 4
      backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs
  44. 4
      backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs
  45. 18
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs
  46. 16
      backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs
  47. 5
      frontend/src/app/features/content/pages/content/content-history-page.component.html
  48. 4
      frontend/src/app/features/content/pages/content/content-page.component.html
  49. 1
      frontend/src/app/features/content/pages/content/content-page.component.ts
  50. 3
      frontend/src/app/features/content/shared/list/content.component.html
  51. 3
      frontend/src/app/features/rules/pages/rule/rule-page.component.html
  52. 3
      frontend/src/app/features/rules/pages/rules/rule.component.html
  53. 3
      frontend/src/app/features/schemas/pages/schema/fields/field.component.html
  54. 3
      frontend/src/app/features/schemas/pages/schema/schema-page.component.html
  55. 2
      frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html
  56. 6
      frontend/src/app/framework/angular/forms/editable-title.component.html
  57. 3
      frontend/src/app/framework/angular/layout.component.html
  58. 4
      frontend/src/app/shared/components/contents/content-status.component.html
  59. 2
      frontend/src/app/shared/components/contents/content-status.component.ts
  60. 12
      frontend/src/app/shared/state/contents.forms.ts
  61. 12
      tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs
  62. 46
      tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs
  63. 16
      tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs
  64. 16
      tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs
  65. 2
      tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs
  66. 19
      tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs
  67. 16
      tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
  68. 151
      tools/TestSuite/TestSuite.ApiTests/AssetScriptingTests.cs
  69. 228
      tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  70. 4
      tools/TestSuite/TestSuite.ApiTests/BackupTests.cs
  71. 4
      tools/TestSuite/TestSuite.ApiTests/CDNTests.cs
  72. 4
      tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs
  73. 12
      tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs
  74. 2
      tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs
  75. 10
      tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  76. 2
      tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs
  77. 6
      tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs
  78. 128
      tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  79. 2
      tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs
  80. 2
      tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs
  81. 50
      tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  82. 16
      tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs
  83. 128
      tools/TestSuite/TestSuite.ApiTests/TeamContributorTests.cs
  84. 31
      tools/TestSuite/TestSuite.ApiTests/TeamCreationTests.cs
  85. 86
      tools/TestSuite/TestSuite.ApiTests/TeamTests.cs
  86. 1
      tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj
  87. 13
      tools/TestSuite/TestSuite.ApiTests/Verify/AppContributorsTests.Should_remove_contributor.verified.txt
  88. 0
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_file_name_strategy=Bulk.verified.txt
  89. 47
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_file_name_strategy=Single.verified.txt
  90. 0
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_metadata_strategy=Bulk.verified.txt
  91. 46
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_metadata_strategy=Single.verified.txt
  92. 0
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_slug_strategy=Bulk.verified.txt
  93. 47
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_slug_strategy=Single.verified.txt
  94. 0
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_tags_strategy=Bulk.verified.txt
  95. 46
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_tags_strategy=Single.verified.txt
  96. 0
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_protect_asset_strategy=Bulk.verified.txt
  97. 47
      tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_protect_asset_strategy=Single.verified.txt
  98. 11
      tools/TestSuite/TestSuite.ApiTests/Verify/TeamContributorTests.Should_invite_contributor.verified.txt
  99. 23
      tools/TestSuite/TestSuite.ApiTests/Verify/TeamContributorTests.Should_remove_contributor.verified.txt
  100. 2
      tools/TestSuite/TestSuite.LoadTests/Run.cs

2
backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectActionHandler.cs

@ -23,7 +23,7 @@ namespace Squidex.Extensions.Actions.DeepDetect;
#pragma warning disable MA0048 // File name must match type name #pragma warning disable MA0048 // File name must match type name
internal partial class DeepDetectActionHandler : RuleActionHandler<DeepDetectAction, DeepDetectJob> internal sealed partial class DeepDetectActionHandler : RuleActionHandler<DeepDetectAction, DeepDetectJob>
{ {
private const string Description = "Analyze Image"; private const string Description = "Analyze Image";
private readonly IHttpClientFactory httpClientFactory; private readonly IHttpClientFactory httpClientFactory;

2
backend/extensions/Squidex.Extensions/Actions/DeepDetect/DeepDetectPlugin.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Actions.DeepDetect; namespace Squidex.Extensions.Actions.DeepDetect;
internal class DeepDetectPlugin : IPlugin internal sealed class DeepDetectPlugin : IPlugin
{ {
public void ConfigureServices(IServiceCollection services, IConfiguration config) public void ConfigureServices(IServiceCollection services, IConfiguration config)
{ {

6
backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs

@ -47,7 +47,11 @@ public sealed class EmailActionHandler : RuleActionHandler<EmailAction, EmailJob
using (var smtpClient = new SmtpClient()) using (var smtpClient = new SmtpClient())
{ {
await smtpClient.ConnectAsync(job.ServerHost, job.ServerPort, cancellationToken: ct); await smtpClient.ConnectAsync(job.ServerHost, job.ServerPort, cancellationToken: ct);
await smtpClient.AuthenticateAsync(job.ServerUsername, job.ServerPassword, ct);
if (!string.IsNullOrWhiteSpace(job.ServerUsername) && !string.IsNullOrWhiteSpace(job.ServerPassword))
{
await smtpClient.AuthenticateAsync(job.ServerUsername, job.ServerPassword, ct);
}
var smtpMessage = new MimeMessage(); var smtpMessage = new MimeMessage();

1
backend/i18n/frontend_en.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Users", "common.users": "Users",
"common.value": "Value", "common.value": "Value",
"common.version": "Version",
"common.weekly": "Weekly", "common.weekly": "Weekly",
"common.width": "Width", "common.width": "Width",
"common.workflow": "Workflow", "common.workflow": "Workflow",

1
backend/i18n/frontend_fr.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Utilisateurs", "common.users": "Utilisateurs",
"common.value": "Valeur", "common.value": "Valeur",
"common.version": "Version",
"common.weekly": "Hebdomadaire", "common.weekly": "Hebdomadaire",
"common.width": "Largeur", "common.width": "Largeur",
"common.workflow": "Flux de travail", "common.workflow": "Flux de travail",

1
backend/i18n/frontend_it.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Utenti", "common.users": "Utenti",
"common.value": "Valore", "common.value": "Valore",
"common.version": "Version",
"common.weekly": "Weekly", "common.weekly": "Weekly",
"common.width": "Larghezza", "common.width": "Larghezza",
"common.workflow": "Workflow", "common.workflow": "Workflow",

1
backend/i18n/frontend_nl.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Gebruikers", "common.users": "Gebruikers",
"common.value": "Waarde", "common.value": "Waarde",
"common.version": "Version",
"common.weekly": "Wekelijks", "common.weekly": "Wekelijks",
"common.width": "Breedte", "common.width": "Breedte",
"common.workflow": "Workflow", "common.workflow": "Workflow",

1
backend/i18n/frontend_pt.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Utilizadores", "common.users": "Utilizadores",
"common.value": "Valor", "common.value": "Valor",
"common.version": "Version",
"common.weekly": "Semanal", "common.weekly": "Semanal",
"common.width": "Largura", "common.width": "Largura",
"common.workflow": "Fluxo de trabalho", "common.workflow": "Fluxo de trabalho",

1
backend/i18n/frontend_zh.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "用户", "common.users": "用户",
"common.value": "值", "common.value": "值",
"common.version": "Version",
"common.weekly": "Weekly", "common.weekly": "Weekly",
"common.width": "宽度", "common.width": "宽度",
"common.workflow": "工作流程", "common.workflow": "工作流程",

1
backend/i18n/source/frontend_en.json

@ -425,6 +425,7 @@
"common.url": "URL", "common.url": "URL",
"common.users": "Users", "common.users": "Users",
"common.value": "Value", "common.value": "Value",
"common.version": "Version",
"common.weekly": "Weekly", "common.weekly": "Weekly",
"common.width": "Width", "common.width": "Width",
"common.workflow": "Workflow", "common.workflow": "Workflow",

8
backend/src/Migrations/OldTriggers/AssetChangedTrigger.cs

@ -35,22 +35,22 @@ public sealed record AssetChangedTrigger : RuleTrigger, IMigrated<RuleTrigger>
if (SendCreate) if (SendCreate)
{ {
conditions.Add($"event.type == '{EnrichedAssetEventType.Created}'"); conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Created)}'");
} }
if (SendUpdate) if (SendUpdate)
{ {
conditions.Add($"event.type == '{EnrichedAssetEventType.Updated}'"); conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Updated)}'");
} }
if (SendRename) if (SendRename)
{ {
conditions.Add($"event.type == '{EnrichedAssetEventType.Annotated}'"); conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Annotated)}'");
} }
if (SendDelete) if (SendDelete)
{ {
conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Deleted)}'");
} }
var condition = string.Empty; var condition = string.Empty;

8
backend/src/Migrations/OldTriggers/ContentChangedTriggerSchema.cs

@ -35,17 +35,17 @@ public sealed class ContentChangedTriggerSchema
if (SendCreate) if (SendCreate)
{ {
conditions.Add($"event.type == '{EnrichedContentEventType.Created}'"); conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Created)}'");
} }
if (SendUpdate) if (SendUpdate)
{ {
conditions.Add($"event.type == '{EnrichedContentEventType.Updated}'"); conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Updated)}'");
} }
if (SendPublish) if (SendPublish)
{ {
conditions.Add($"event.type == '{EnrichedContentEventType.Published}'"); conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Published)}'");
} }
if (SendArchived) if (SendArchived)
@ -55,7 +55,7 @@ public sealed class ContentChangedTriggerSchema
if (SendDelete) if (SendDelete)
{ {
conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'"); conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Deleted)}'");
} }
var condition = string.Empty; var condition = string.Empty;

18
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -204,15 +204,6 @@ namespace Squidex.Domain.Apps.Core {
} }
} }
/// <summary>
/// Looks up a localized string similar to The asset metadata with name &apos;name&apos;..
/// </summary>
public static string AssetMetadataValue {
get {
return ResourceManager.GetString("AssetMetadataValue", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The mime type.. /// Looks up a localized string similar to The mime type..
/// </summary> /// </summary>
@ -816,6 +807,15 @@ namespace Squidex.Domain.Apps.Core {
} }
} }
/// <summary>
/// Looks up a localized string similar to The value with name &apos;name&apos;..
/// </summary>
public static string ObjectValue {
get {
return ResourceManager.GetString("ObjectValue", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to The current operation.. /// Looks up a localized string similar to The current operation..
/// </summary> /// </summary>

6
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -165,9 +165,6 @@
<data name="AssetMetadataText" xml:space="preserve"> <data name="AssetMetadataText" xml:space="preserve">
<value>The type of the image.</value> <value>The type of the image.</value>
</data> </data>
<data name="AssetMetadataValue" xml:space="preserve">
<value>The asset metadata with name 'name'.</value>
</data>
<data name="AssetMimeType" xml:space="preserve"> <data name="AssetMimeType" xml:space="preserve">
<value>The mime type.</value> <value>The mime type.</value>
</data> </data>
@ -369,6 +366,9 @@
<data name="NamedName" xml:space="preserve"> <data name="NamedName" xml:space="preserve">
<value>The name part of this ID.</value> <value>The name part of this ID.</value>
</data> </data>
<data name="ObjectValue" xml:space="preserve">
<value>The value with name 'name'.</value>
</data>
<data name="Operation" xml:space="preserve"> <data name="Operation" xml:space="preserve">
<value>The current operation.</value> <value>The current operation.</value>
</data> </data>

10
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetEntityScriptVars.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting; namespace Squidex.Domain.Apps.Core.Scripting;
@ -51,15 +51,15 @@ public sealed class AssetEntityScriptVars : ScriptVars
} }
[FieldDescription(nameof(FieldDescriptions.AssetMetadata))] [FieldDescription(nameof(FieldDescriptions.AssetMetadata))]
public AssetMetadata? Metadata public ReadonlyDictionary<string, JsonValue>? Metadata
{ {
set => SetInitial(value != null ? new ReadOnlyDictionary<string, JsonValue>(value) : null); set => SetInitial(value);
} }
[FieldDescription(nameof(FieldDescriptions.AssetTags))] [FieldDescription(nameof(FieldDescriptions.AssetTags))]
public HashSet<string>? Tags public ReadonlyList<string>? Tags
{ {
set => SetInitial(value != null ? new ReadOnlyCollection<string>(value.ToList()) : null); set => SetInitial(value);
} }
[FieldDescription(nameof(FieldDescriptions.AssetFileSize))] [FieldDescription(nameof(FieldDescriptions.AssetFileSize))]

4
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -70,7 +70,7 @@ public sealed class ContentDataObject : ObjectInstance
{ {
var propertyName = property.AsString(); var propertyName = property.AsString();
fieldsToDelete ??= new HashSet<string>(); fieldsToDelete ??= [];
fieldsToDelete.Add(propertyName); fieldsToDelete.Add(propertyName);
fieldProperties?.Remove(propertyName); fieldProperties?.Remove(propertyName);
@ -114,7 +114,7 @@ public sealed class ContentDataObject : ObjectInstance
return PropertyDescriptor.Undefined; return PropertyDescriptor.Undefined;
} }
return fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)), this); return fieldProperties.GetOrAdd(propertyName, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, [], false)), this);
} }
public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties() public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs

@ -35,7 +35,7 @@ public sealed class ContentDataProperty : CustomProperty
var obj = value.AsObject(); var obj = value.AsObject();
contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); contentField = new ContentFieldObject(contentData, [], true);
foreach (var (key, propertyDescriptor) in obj.GetOwnProperties()) foreach (var (key, propertyDescriptor) in obj.GetOwnProperties())
{ {

2
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -85,7 +85,7 @@ public sealed class ContentFieldObject : ObjectInstance
{ {
var propertyName = property.AsString(); var propertyName = property.AsString();
valuesToDelete ??= new HashSet<string>(); valuesToDelete ??= [];
valuesToDelete.Add(propertyName); valuesToDelete.Add(propertyName);
valueProperties?.Remove(propertyName); valueProperties?.Remove(propertyName);

33
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs

@ -11,7 +11,6 @@ using System.Reflection;
using System.Security.Claims; using System.Security.Claims;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -116,7 +115,7 @@ public sealed partial class ScriptingCompleter
{ {
private static readonly Regex RegexProperty = BuildPropertyRegex(); private static readonly Regex RegexProperty = BuildPropertyRegex();
private readonly Stack<string> prefixes = new Stack<string>(); private readonly Stack<string> prefixes = new Stack<string>();
private readonly Dictionary<string, ScriptingValue> result = new Dictionary<string, ScriptingValue>(); private readonly Dictionary<string, ScriptingValue> result = [];
private readonly IEnumerable<IScriptDescriptor> descriptors; private readonly IEnumerable<IScriptDescriptor> descriptors;
private readonly FilterSchema? dataSchema; private readonly FilterSchema? dataSchema;
@ -280,7 +279,7 @@ public sealed partial class ScriptingCompleter
if (propertyType.IsEnum) if (propertyType.IsEnum)
{ {
var allowedValues = Enum.GetValues(propertyType).OfType<object>().Select(x => x.ToString()!).Order().ToArray(); var allowedValues = Enum.GetNames(propertyType);
Add(JsonType.String, name, description, allowedValues); Add(JsonType.String, name, description, allowedValues);
} }
@ -304,14 +303,6 @@ public sealed partial class ScriptingCompleter
{ {
AddFunction(name, description); AddFunction(name, description);
} }
else if (propertyType == typeof(AssetMetadata))
{
AddObject(name, description, () =>
{
AddString("my-name",
FieldDescriptions.AssetMetadataValue);
});
}
else if (propertyType == typeof(NamedId<DomainId>)) else if (propertyType == typeof(NamedId<DomainId>))
{ {
AddObject(name, description, () => AddObject(name, description, () =>
@ -371,13 +362,31 @@ public sealed partial class ScriptingCompleter
AddType(propertyType); AddType(propertyType);
}); });
} }
else if (propertyType.GetInterfaces().Contains(typeof(IEnumerable))) else if (IsDictionary(propertyType))
{
AddObject(name, description, () =>
{
AddString("my-name",
FieldDescriptions.ObjectValue);
});
}
else if (IsCollection(propertyType))
{ {
AddArray(name, description); AddArray(name, description);
} }
} }
} }
private static bool IsDictionary(Type propertyType)
{
return propertyType.GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>));
}
private static bool IsCollection(Type propertyType)
{
return propertyType.GetInterfaces().Contains(typeof(IEnumerable));
}
private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type) private static IEnumerable<(string Name, string Description, Type Type)> GetFields(Type type)
{ {
foreach (var property in type.GetPublicProperties()) foreach (var property in type.GetPublicProperties())

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Text/CommandFactory.cs

@ -57,7 +57,7 @@ public sealed class CommandFactory<T> : MongoBase<MongoTextIndexEntity<T>> where
IsUpsert = true IsUpsert = true
}); });
if (upsert.GeoObjects?.Any() == true) if (upsert.GeoObjects?.Count > 0)
{ {
if (!upsert.IsNew) if (!upsert.IsNew)
{ {

9
backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/Guards/ScriptingExtensions.cs

@ -8,6 +8,7 @@
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Text; using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards;
@ -62,11 +63,11 @@ public static class ScriptingExtensions
FileSize = create.File.FileSize, FileSize = create.File.FileSize,
FileVersion = 0, FileVersion = 0,
IsProtected = false, IsProtected = false,
Metadata = create.Metadata, Metadata = create.Metadata?.ToReadonlyDictionary(),
MimeType = create.File.MimeType, MimeType = create.File.MimeType,
ParentId = create.ParentId, ParentId = create.ParentId,
ParentPath = await GetPathAsync(operation, create.ParentId, ct), ParentPath = await GetPathAsync(operation, create.ParentId, ct),
Tags = create.Tags Tags = create.Tags?.ToReadonlyList()
}; };
await ExecuteScriptAsync(operation, script, vars, asset, ct); await ExecuteScriptAsync(operation, script, vars, asset, ct);
@ -194,11 +195,11 @@ public static class ScriptingExtensions
FileSlug = snapshot.Slug, FileSlug = snapshot.Slug,
FileVersion = snapshot.FileVersion, FileVersion = snapshot.FileVersion,
IsProtected = snapshot.IsProtected, IsProtected = snapshot.IsProtected,
Metadata = snapshot.Metadata, Metadata = snapshot.Metadata?.ToReadonlyDictionary(),
MimeType = snapshot.MimeType, MimeType = snapshot.MimeType,
ParentId = snapshot.ParentId, ParentId = snapshot.ParentId,
ParentPath = await GetPathAsync(operation, snapshot.ParentId, ct), ParentPath = await GetPathAsync(operation, snapshot.ParentId, ct),
Tags = snapshot.Tags, Tags = snapshot.Tags?.ToReadonlyList(),
}; };
vars.AppId = operation.App.Id; vars.AppId = operation.App.Id;

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/ScriptAsset.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure.Collections;
using Squidex.Shared; using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Assets.Queries.Steps; namespace Squidex.Domain.Apps.Entities.Assets.Queries.Steps;
@ -76,11 +77,11 @@ public sealed class ScriptAsset : IAssetEnricherStep
FileSlug = asset.Slug, FileSlug = asset.Slug,
FileVersion = asset.FileVersion, FileVersion = asset.FileVersion,
IsProtected = asset.IsProtected, IsProtected = asset.IsProtected,
Metadata = asset.Metadata, Metadata = asset.Metadata?.ToReadonlyDictionary(),
MimeType = asset.MimeType, MimeType = asset.MimeType,
ParentId = asset.ParentId, ParentId = asset.ParentId,
ParentPath = null, ParentPath = null,
Tags = asset.Tags Tags = asset.Tags?.ToReadonlyList()
} }
}; };

7
backend/src/Squidex.Domain.Apps.Entities/Collaboration/EmailUserNotifications.cs

@ -47,11 +47,8 @@ public sealed class EmailUserNotifications : IUserNotifications
get => true; get => true;
} }
public EmailUserNotifications( public EmailUserNotifications(IOptions<EmailUserNotificationOptions> texts, IEmailSender emailSender,
IOptions<EmailUserNotificationOptions> texts, IUrlGenerator urlGenerator, ILogger<EmailUserNotifications> log)
IEmailSender emailSender,
IUrlGenerator urlGenerator,
ILogger<EmailUserNotifications> log)
{ {
this.texts = texts.Value; this.texts = texts.Value;
this.emailSender = emailSender; this.emailSender = emailSender;

3
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -54,7 +54,4 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Notifications\" />
</ItemGroup>
</Project> </Project>

2
backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs

@ -72,7 +72,7 @@ public sealed class SmtpEmailSender : IEmailSender
await smtpClient.ConnectAsync(options.Server, options.Port, cancellationToken: ct); await smtpClient.ConnectAsync(options.Server, options.Port, cancellationToken: ct);
} }
if (!smtpClient.IsAuthenticated) if (!smtpClient.IsAuthenticated && options.IsAuthenticating())
{ {
await smtpClient.AuthenticateAsync(options.Username, options.Password, ct); await smtpClient.AuthenticateAsync(options.Username, options.Password, ct);
} }

7
backend/src/Squidex.Infrastructure/Email/SmtpOptions.cs

@ -27,7 +27,12 @@ public sealed class SmtpOptions
{ {
return return
!string.IsNullOrWhiteSpace(Server) && !string.IsNullOrWhiteSpace(Server) &&
!string.IsNullOrWhiteSpace(Sender) && !string.IsNullOrWhiteSpace(Sender);
}
public bool IsAuthenticating()
{
return
!string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Username) &&
!string.IsNullOrWhiteSpace(Password); !string.IsNullOrWhiteSpace(Password);
} }

73
backend/src/Squidex.Infrastructure/HashExtensions.cs

@ -202,6 +202,15 @@ public static class HashExtensions
return ToHexString(buffer); return ToHexString(buffer);
} }
public static string GetQuotedHexStringAndReset(this IncrementalHash hash)
{
Span<byte> buffer = stackalloc byte[hash.HashLengthInBytes];
hash.GetHashAndReset(buffer);
return ToQuotedHexString(buffer);
}
public static string ToHexString(this ReadOnlySpan<byte> data) public static string ToHexString(this ReadOnlySpan<byte> data)
{ {
string result; string result;
@ -213,7 +222,7 @@ public static class HashExtensions
var buffer = ArrayPool<char>.Shared.Rent(length); var buffer = ArrayPool<char>.Shared.Rent(length);
try try
{ {
result = ConvertCore(data, buffer); result = ConvertCore(data, buffer, 0);
} }
finally finally
{ {
@ -224,29 +233,63 @@ public static class HashExtensions
{ {
Span<char> buffer = stackalloc char[length]; Span<char> buffer = stackalloc char[length];
result = ConvertCore(data, buffer); result = ConvertCore(data, buffer, 0);
} }
return result; return result;
}
static string ConvertCore(ReadOnlySpan<byte> data, Span<char> hexBuffer) public static string ToQuotedHexString(this ReadOnlySpan<byte> data)
{
string result;
var length = (data.Length * 2) + 2;
if (length > MaxStackSize)
{ {
unchecked var buffer = ArrayPool<char>.Shared.Rent(length);
try
{ {
int n = 0; buffer[0] = '"';
for (int i = 0; i < data.Length; i++) buffer[^1] = '"';
{
byte b = data[i];
byte b1 = (byte)(b >> 4); result = ConvertCore(data, buffer, 1);
byte b2 = (byte)(b & 0xF);
hexBuffer[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10));
hexBuffer[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10));
}
} }
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
else
{
Span<char> buffer = stackalloc char[length];
buffer[0] = '"';
buffer[^1] = '"';
result = ConvertCore(data, buffer, 1);
}
return result;
}
return new string(hexBuffer); private static string ConvertCore(ReadOnlySpan<byte> data, Span<char> hexBuffer, int offset)
{
unchecked
{
int n = offset;
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
byte b1 = (byte)(b >> 4);
byte b2 = (byte)(b & 0xF);
hexBuffer[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10));
hexBuffer[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10));
}
} }
return new string(hexBuffer);
} }
} }

18
backend/src/Squidex.Web/BuilderExtensions.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Builder;
namespace Squidex.Web;
public static class BuilderExtensions
{
public static void UseWhenPath(this IApplicationBuilder builder, string path, Action<IApplicationBuilder> configurator)
{
builder.UseWhen(c => c.Request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase), configurator);
}
}

22
backend/src/Squidex.Web/ETagExtensions.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System.Globalization; using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Cryptography; using System.Security.Cryptography;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -60,21 +61,18 @@ public static class ETagExtensions
{ {
version = default; version = default;
if (httpContext.Request.Headers.TryGetString(header, out var etag)) if (!httpContext.Request.Headers.TryGetValue(header, out var etagValue))
{ {
var span = etag.AsSpan(); return false;
}
if (span.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
span = span[2..];
}
if (long.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture, out version)) if (!EntityTagHeaderValue.TryParse(etagValue.ToString(), out var etag))
{ {
return true; return false;
}
} }
return false; var tag = etag.Tag.AsSpan().Trim('"');
return long.TryParse(tag, NumberStyles.Any, CultureInfo.InvariantCulture, out version);
} }
} }

58
backend/src/Squidex.Web/ETagUtils.cs

@ -1,58 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Web;
public static class ETagUtils
{
public static string ToWeakEtag(string? etag)
{
return $"W/{etag}";
}
public static bool IsStrongEtag(string etag)
{
return !IsWeakEtag(etag.AsSpan());
}
public static bool IsWeakEtag(string etag)
{
return IsWeakEtag(etag.AsSpan());
}
public static bool IsWeakEtag(ReadOnlySpan<char> etag)
{
return etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase);
}
public static bool IsSameEtag(string lhs, string rhs)
{
return IsSameEtag(lhs.AsSpan(), rhs.AsSpan());
}
public static bool IsSameEtag(ReadOnlySpan<char> lhs, ReadOnlySpan<char> rhs)
{
var isMatch = lhs.Equals(rhs, StringComparison.Ordinal);
if (isMatch)
{
return true;
}
if (lhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
lhs = lhs[2..];
}
if (rhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
rhs = rhs[2..];
}
return lhs.Equals(rhs, StringComparison.Ordinal);
}
}

20
backend/src/Squidex.Web/Extensions.cs

@ -5,9 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
namespace Squidex.Web; namespace Squidex.Web;
@ -56,22 +54,4 @@ public static class Extensions
return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase); return string.Equals(subject, userId, StringComparison.OrdinalIgnoreCase);
} }
public static bool TryGetString(this IHeaderDictionary headers, string header, [MaybeNullWhen(false)] out string result)
{
result = null!;
if (headers.TryGetValue(header, out var value))
{
string? valueString = value;
if (!string.IsNullOrWhiteSpace(valueString))
{
result = valueString;
return true;
}
}
return false;
}
} }

14
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -38,26 +38,32 @@ public sealed class CachingFilter : IAsyncActionFilter
cachingManager.Finish(context.HttpContext); cachingManager.Finish(context.HttpContext);
if (context.HttpContext.Response.HasStarted == false && if (context.HttpContext.Response.HasStarted == false &&
context.HttpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag) && context.HttpContext.Response.Headers.TryGetValue(HeaderNames.ETag, out var etagValue) &&
EntityTagHeaderValue.TryParse(etagValue.ToString(), out var etag) &&
IsCacheable(context.HttpContext, etag)) IsCacheable(context.HttpContext, etag))
{ {
resultContext.Result = new StatusCodeResult(304); resultContext.Result = new StatusCodeResult(304);
} }
} }
private static bool IsCacheable(HttpContext httpContext, string etag) private static bool IsCacheable(HttpContext httpContext, EntityTagHeaderValue etag)
{ {
if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200) if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200)
{ {
return false; return false;
} }
if (!httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatchValue)) if (!httpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var noneMatchValue))
{ {
return false; return false;
} }
return ETagUtils.IsSameEtag(noneMatchValue, etag); if (!EntityTagHeaderValue.TryParse(noneMatchValue.ToString(), out var noneMatch))
{
return false;
}
return etag.Compare(noneMatch, false);
} }
private static bool IgnoreFilter(ActionExecutingContext context) private static bool IgnoreFilter(ActionExecutingContext context)

6
backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs

@ -37,11 +37,11 @@ public sealed class CachingKeysMiddleware
cachingManager.Finish(httpContext); cachingManager.Finish(httpContext);
if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) if (httpContext.Response.Headers.TryGetValue(HeaderNames.ETag, out var value) && EntityTagHeaderValue.TryParse(value.ToString(), out var etag))
{ {
if (!cachingOptions.StrongETag && !ETagUtils.IsWeakEtag(etag)) if (!cachingOptions.StrongETag && !etag.IsWeak)
{ {
httpContext.Response.Headers[HeaderNames.ETag] = ETagUtils.ToWeakEtag(etag); httpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue(etag.Tag, true).ToString();
} }
} }

2
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -117,7 +117,7 @@ public sealed class CachingManager : IRequestCache
{ {
using (Telemetry.Activities.StartActivity("CalculateEtag")) using (Telemetry.Activities.StartActivity("CalculateEtag"))
{ {
response.Headers[HeaderNames.ETag] = hasher.GetHexStringAndReset(); response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue(hasher.GetQuotedHexStringAndReset()).ToString();
} }
} }

10
backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs

@ -63,7 +63,7 @@ public static class IndexExtensions
private static string GetText(string culture) private static string GetText(string culture)
{ {
if (!Texts.TryGetValue(culture, out var result)) return Texts.GetOrAdd(culture, culture =>
{ {
var assembly = typeof(IndexExtensions).Assembly; var assembly = typeof(IndexExtensions).Assembly;
@ -74,17 +74,15 @@ public static class IndexExtensions
{ {
using (var reader = new StreamReader(resourceStream)) using (var reader = new StreamReader(resourceStream))
{ {
result = reader.ReadToEnd(); var result = reader.ReadToEnd();
Texts[culture] = result; return result;
} }
} }
else else
{ {
return GetText("en"); return GetText("en");
} }
} });
return result;
} }
} }

30
backend/src/Squidex/Areas/Frontend/Startup.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Squidex.Areas.Frontend.Middlewares; using Squidex.Areas.Frontend.Middlewares;
@ -40,12 +41,12 @@ public static class Startup
app.UseMiddleware<NotifoMiddleware>(); app.UseMiddleware<NotifoMiddleware>();
app.UseWhen(c => c.IsSpaFile(), builder => app.UseWhen(IsSpaFile, builder =>
{ {
builder.UseMiddleware<SetupMiddleware>(); builder.UseMiddleware<SetupMiddleware>();
}); });
app.UseWhen(c => c.IsSpaFile() || c.IsHtmlPath(), builder => app.UseWhen(IsSpaFileOrHtml, builder =>
{ {
// Adjust the base for all potential html files. // Adjust the base for all potential html files.
builder.UseHtmlTransform(new HtmlTransformOptions builder.UseHtmlTransform(new HtmlTransformOptions
@ -82,25 +83,42 @@ public static class Startup
private static void UseSquidexStaticFiles(this IApplicationBuilder app, IFileProvider fileProvider) private static void UseSquidexStaticFiles(this IApplicationBuilder app, IFileProvider fileProvider)
{ {
static bool HasQueryString(HttpContext context)
{
return !string.IsNullOrWhiteSpace(context.Request.QueryString.ToString());
}
static bool IsHtml(HttpContext context)
{
return string.Equals(context.Response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase);
}
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
OnPrepareResponse = context => OnPrepareResponse = context =>
{ {
var response = context.Context.Response; var response = context.Context.Response;
if (!string.IsNullOrWhiteSpace(context.Context.Request.QueryString.ToString())) if (IsHtml(context.Context) || HasQueryString(context.Context))
{ {
response.Headers[HeaderNames.CacheControl] = "max-age=5184000"; response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers.Remove(HeaderNames.ETag);
response.Headers.Remove(HeaderNames.LastModified);
} }
else if (string.Equals(response.ContentType, "text/html", StringComparison.OrdinalIgnoreCase)) else
{ {
response.Headers[HeaderNames.CacheControl] = "no-cache"; response.Headers[HeaderNames.CacheControl] = "max-age=5184000";
} }
}, },
FileProvider = fileProvider FileProvider = fileProvider
}); });
} }
private static bool IsSpaFileOrHtml(this HttpContext context)
{
return context.IsSpaFile() || context.IsHtmlPath();
}
private static bool IsSpaFile(this HttpContext context) private static bool IsSpaFile(this HttpContext context)
{ {
return (context.IsIndex() || !Path.HasExtension(context.Request.Path)) && !context.IsDevServer(); return (context.IsIndex() || !Path.HasExtension(context.Request.Path)) && !context.IsDevServer();

7
backend/src/Squidex/Squidex.csproj

@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<NoWarn>1701;1702;NETSDK1206;NU1608</NoWarn> <NoWarn>1701;1702;CS1591;NETSDK1206;NU1608</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -136,6 +136,7 @@
<EmbeddedResource Include="..\..\i18n\frontend_nl.json" Link="Areas\Frontend\Resources\frontend_nl.json" /> <EmbeddedResource Include="..\..\i18n\frontend_nl.json" Link="Areas\Frontend\Resources\frontend_nl.json" />
<EmbeddedResource Include="..\..\i18n\frontend_pt.json" Link="Areas\Frontend\Resources\frontend_pt.json" /> <EmbeddedResource Include="..\..\i18n\frontend_pt.json" Link="Areas\Frontend\Resources\frontend_pt.json" />
<EmbeddedResource Include="..\..\i18n\frontend_zh.json" Link="Areas\Frontend\Resources\frontend_zh.json" /> <EmbeddedResource Include="..\..\i18n\frontend_zh.json" Link="Areas\Frontend\Resources\frontend_zh.json" />
<EmbeddedResource Include="..\..\i18n\frontend_fr.json" Link="Areas\Frontend\Resources\frontend_fr.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -156,8 +157,4 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput> <LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn>
</PropertyGroup>
</Project> </Project>

4
backend/src/Squidex/Startup.cs

@ -96,13 +96,13 @@ public sealed class Startup
if (!app.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment()) if (!app.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsDevelopment())
{ {
app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixIdentityServer, StringComparison.OrdinalIgnoreCase), builder => app.UseWhenPath(Constants.PrefixIdentityServer, builder =>
{ {
builder.UseExceptionHandler("/identity-server/error"); builder.UseExceptionHandler("/identity-server/error");
}); });
} }
app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixApi, StringComparison.OrdinalIgnoreCase), builder => app.UseWhenPath(Constants.PrefixApi, builder =>
{ {
builder.UseSquidexCacheKeys(); builder.UseSquidexCacheKeys();
builder.UseSquidexExceptionHandling(); builder.UseSquidexExceptionHandling();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Billing/ConfigPlansProviderTests.cs

@ -56,7 +56,7 @@ public class ConfigPlansProviderTests
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("my-plan")] [InlineData("my-plan")]
public void Should_return_infinite_if_nothing_configured(string planId) public void Should_return_infinite_if_nothing_configured(string? planId)
{ {
var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>()); var sut = new ConfigPlansProvider(Enumerable.Empty<Plan>());

4
backend/tests/Squidex.Infrastructure.Tests/FileExtensionsTests.cs

@ -27,9 +27,9 @@ public class FileExtensionsTests
[InlineData("")] [InlineData("")]
[InlineData(" ")] [InlineData(" ")]
[InlineData(null)] [InlineData(null)]
public void Should_blob_for_invalid_file_types(string fileName) public void Should_blob_for_invalid_file_types(string? fileName)
{ {
var actual = fileName.FileType(); var actual = fileName!.FileType();
Assert.Equal("blob", actual); Assert.Equal("blob", actual);
} }

28
backend/tests/Squidex.Infrastructure.Tests/HashExtensionsTests.cs

@ -107,6 +107,34 @@ public class HashExtensionsTests
Assert.Equal(hash1, hash2); Assert.Equal(hash1, hash2);
} }
[Fact]
public void Should_calculate_incremental_hash()
{
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
hash.AppendString($"{Guid.Empty}-{Guid.Empty}");
hash.AppendString($"{Guid.Empty}-{Guid.Empty}-{Guid.Empty}-{Guid.Empty}");
hash.AppendLong(42);
var result = hash.GetHexStringAndReset();
Assert.Equal("62D6F977A8C2B79983A489AA5932C512", result);
}
[Fact]
public void Should_calculate_incremental_quoted_hash()
{
using var hash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
hash.AppendString($"{Guid.Empty}-{Guid.Empty}");
hash.AppendString($"{Guid.Empty}-{Guid.Empty}-{Guid.Empty}-{Guid.Empty}");
hash.AppendLong(42);
var result = hash.GetQuotedHexStringAndReset();
Assert.Equal("\"62D6F977A8C2B79983A489AA5932C512\"", result);
}
private delegate string HashFromBytes(ReadOnlySpan<byte> source); private delegate string HashFromBytes(ReadOnlySpan<byte> source);
private static void AssertHashFromBytes(HashFromBytes hasher, int length) private static void AssertHashFromBytes(HashFromBytes hasher, int length)

4
backend/tests/Squidex.Infrastructure.Tests/LanguageTests.cs

@ -127,9 +127,9 @@ public class LanguageTests
[InlineData("xx")] [InlineData("xx")]
[InlineData("invalid")] [InlineData("invalid")]
[InlineData(null)] [InlineData(null)]
public void Should_parse_invalid_languages(string input) public void Should_parse_invalid_languages(string? input)
{ {
var language = Language.ParseOrNull(input); var language = Language.ParseOrNull(input!);
Assert.Null(language); Assert.Null(language);
} }

4
backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs

@ -16,7 +16,7 @@ public class StringExtensionsTests
[InlineData("me@@web.com", false)] [InlineData("me@@web.com", false)]
[InlineData("me@web.com", true)] [InlineData("me@web.com", true)]
[InlineData("Me@web.com", true)] [InlineData("Me@web.com", true)]
public void Should_check_email(string email, bool isEmail) public void Should_check_email(string? email, bool isEmail)
{ {
Assert.Equal(isEmail, email.IsEmail()); Assert.Equal(isEmail, email.IsEmail());
} }
@ -25,7 +25,7 @@ public class StringExtensionsTests
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
[InlineData(" ")] [InlineData(" ")]
public void Should_provide_fallback_if_invalid(string value) public void Should_provide_fallback_if_invalid(string? value)
{ {
Assert.Equal("fallback", value.Or("fallback")); Assert.Equal("fallback", value.Or("fallback"));
} }

4
backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs

@ -51,7 +51,7 @@ public class ETagCommandMiddlewareTests
[Fact] [Fact]
public async Task Should_add_expected_version_to_command() public async Task Should_add_expected_version_to_command()
{ {
httpContext.Request.Headers[HeaderNames.IfMatch] = "13"; httpContext.Request.Headers[HeaderNames.IfMatch] = "\"13\"";
var context = await HandleAsync(new CreateContent(), true); var context = await HandleAsync(new CreateContent(), true);
@ -61,7 +61,7 @@ public class ETagCommandMiddlewareTests
[Fact] [Fact]
public async Task Should_add_weak_etag_as_expected_version_to_command() public async Task Should_add_weak_etag_as_expected_version_to_command()
{ {
httpContext.Request.Headers[HeaderNames.IfMatch] = "W/13"; httpContext.Request.Headers[HeaderNames.IfMatch] = "W/\"13\"";
var context = await HandleAsync(new CreateContent(), true); var context = await HandleAsync(new CreateContent(), true);

18
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -45,10 +45,10 @@ public class CachingFilterTests
} }
[Theory] [Theory]
[InlineData("13", "13")] [InlineData("\"13\"", "\"13\"")]
[InlineData("13", "W/13")] [InlineData("\"13\"", "W/\"13\"")]
[InlineData("W/13", "13")] [InlineData("W/\"13\"", "\"13\"")]
[InlineData("W/13", "W/13")] [InlineData("W/\"13\"", "W/\"13\"")]
public async Task Should_return_304_for_same_etags(string ifNoneMatch, string etag) public async Task Should_return_304_for_same_etags(string ifNoneMatch, string etag)
{ {
httpContext.Request.Method = HttpMethods.Get; httpContext.Request.Method = HttpMethods.Get;
@ -62,10 +62,10 @@ public class CachingFilterTests
} }
[Theory] [Theory]
[InlineData("13", "13")] [InlineData("\"13\"", "\"13\"")]
[InlineData("13", "W/13")] [InlineData("\"13\"", "W/\"13\"")]
[InlineData("W/13", "13")] [InlineData("W/\"13\"", "\"13\"")]
[InlineData("W/13", "W/13")] [InlineData("W/\"13\"", "W/\"13\"")]
public async Task Should_not_return_304_for_same_etags_when_disabled_via_metadata(string ifNoneMatch, string etag) public async Task Should_not_return_304_for_same_etags_when_disabled_via_metadata(string ifNoneMatch, string etag)
{ {
executingContext.ActionDescriptor.EndpointMetadata = new List<object> executingContext.ActionDescriptor.EndpointMetadata = new List<object>
@ -87,7 +87,7 @@ public class CachingFilterTests
public async Task Should_return_304_for_same_etags_from_cache_manager() public async Task Should_return_304_for_same_etags_from_cache_manager()
{ {
httpContext.Request.Method = HttpMethods.Get; httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "2C70E12B7A0646F92279F427C7B38E7334D8E5389CFF167A1DC30E73F826B683"; httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"2C70E12B7A0646F92279F427C7B38E7334D8E5389CFF167A1DC30E73F826B683\"";
await sut.OnActionExecutionAsync(executingContext, () => await sut.OnActionExecutionAsync(executingContext, () =>
{ {

16
backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs

@ -170,10 +170,10 @@ public class CachingKeysMiddlewareTests
await MakeRequestAsync(() => await MakeRequestAsync(() =>
{ {
httpContext.Response.Headers[HeaderNames.ETag] = "13"; httpContext.Response.Headers[HeaderNames.ETag] = "\"13\"";
}); });
Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]); Assert.Equal("\"13\"", httpContext.Response.Headers[HeaderNames.ETag]);
} }
[Fact] [Fact]
@ -181,10 +181,10 @@ public class CachingKeysMiddlewareTests
{ {
await MakeRequestAsync(() => await MakeRequestAsync(() =>
{ {
httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; httpContext.Response.Headers[HeaderNames.ETag] = "W/\"13\"";
}); });
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); Assert.Equal("W/\"13\"", httpContext.Response.Headers[HeaderNames.ETag]);
} }
[Fact] [Fact]
@ -192,10 +192,10 @@ public class CachingKeysMiddlewareTests
{ {
await MakeRequestAsync(() => await MakeRequestAsync(() =>
{ {
httpContext.Response.Headers[HeaderNames.ETag] = "13"; httpContext.Response.Headers[HeaderNames.ETag] = "\"13\"";
}); });
Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); Assert.Equal("W/\"13\"", httpContext.Response.Headers[HeaderNames.ETag]);
} }
[Fact] [Fact]
@ -358,7 +358,7 @@ public class CachingKeysMiddlewareTests
var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString();
Assert.True(ETagUtils.IsWeakEtag(etag)); Assert.True(EntityTagHeaderValue.TryParse(etag, out var parsed) && parsed.IsWeak);
Assert.True(etag.Length > 20); Assert.True(etag.Length > 20);
} }
@ -379,7 +379,7 @@ public class CachingKeysMiddlewareTests
var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString(); var etag = httpContext.Response.Headers[HeaderNames.ETag].ToString();
Assert.True(ETagUtils.IsStrongEtag(etag)); Assert.True(EntityTagHeaderValue.TryParse(etag, out var parsed) && !parsed.IsWeak);
Assert.True(etag.Length > 20); Assert.True(etag.Length > 20);
} }

5
frontend/src/app/features/content/pages/content/content-history-page.component.html

@ -1,6 +1,6 @@
<sqx-layout layout="right" titleText="i18n:common.workflow" width="20" white="true" overflow="true" padding="true"> <sqx-layout layout="right" titleText="i18n:common.workflow" width="20" white="true" overflow="true" padding="true">
<ng-container> <ng-container>
<div class="section mb-4"> <div class="section mb-2">
<label for="id">{{ 'common.id' | sqxTranslate }}</label> <label for="id">{{ 'common.id' | sqxTranslate }}</label>
<div class="input-group"> <div class="input-group">
@ -11,6 +11,9 @@
</button> </button>
</div> </div>
</div> </div>
<div class="section mb-4">
<label for="version">{{ 'common.version' | sqxTranslate }}</label>: <span id="version">{{content.version}}</span>
</div>
<div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete"> <div class="section mb-4" *ngIf="content.canDraftCreate || content.canDraftDelete">
<ng-container *ngIf="!content.newStatus; else newVersion"> <ng-container *ngIf="!content.newStatus; else newVersion">

4
frontend/src/app/features/content/pages/content/content-page.component.html

@ -4,8 +4,8 @@
<sqx-layout layout="main" [hideSidebar]="!content"> <sqx-layout layout="main" [hideSidebar]="!content">
<ng-container title> <ng-container title>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a class="btn btn-text-secondary" (click)="back()" *ngIf="schema.type !== 'Singleton'"> <a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()" *ngIf="schema.type !== 'Singleton'">
<span class="hidden">{{ 'common.back' | sqxTranslate }}</span> <span id="content-back" hidden>{{ 'common.back' | sqxTranslate }}</span>
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</a> </a>

1
frontend/src/app/features/content/pages/content/content-page.component.ts

@ -267,6 +267,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
.subscribe({ .subscribe({
next: content => { next: content => {
this.contentForm.submitCompleted({ noReset: true }); this.contentForm.submitCompleted({ noReset: true });
this.contentForm.load(content.data, true);
this.router.navigate([content.id, 'history'], { relativeTo: this.route.parent! }); this.router.navigate([content.id, 'history'], { relativeTo: this.route.parent! });
}, },

3
frontend/src/app/features/content/shared/list/content.component.html

@ -22,8 +22,7 @@
</td> </td>
<td class="cell-actions cell-actions-left" sqxStopClick> <td class="cell-actions cell-actions-left" sqxStopClick>
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" #buttonOptions> <button type="button" class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>

3
frontend/src/app/features/rules/pages/rule/rule-page.component.html

@ -5,8 +5,7 @@
<sqx-layout layout="main" innerWidth="54"> <sqx-layout layout="main" innerWidth="54">
<ng-container title> <ng-container title>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a class="btn btn-text-secondary" data-testid="back" (click)="back()"> <a class="btn btn-text-secondary" attr.aria-label="{{ 'common.back' | sqxTranslate }}" (click)="back()">
<span class="hidden">{{ 'common.back' | sqxTranslate }}</span>
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</a> </a>

3
frontend/src/app/features/rules/pages/rules/rule.component.html

@ -12,8 +12,7 @@
</sqx-editable-title> </sqx-editable-title>
</div> </div>
<div class="col-auto" [class.invisible]="!rule.canDelete && !rule.canRun"> <div class="col-auto" [class.invisible]="!rule.canDelete && !rule.canRun">
<button type="button" class="btn btn-text-secondary" data-testid="options" (click)="dropdown.toggle()" #buttonOptions> <button type="button" class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>

3
frontend/src/app/features/schemas/pages/schema/fields/field.component.html

@ -41,8 +41,7 @@
<i class="icon-settings"></i> <i class="icon-settings"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary ms-1" (click)="dropdown.toggle()" data-testid="options" [disabled]="!field.properties.isContentField && field.isLocked" #buttonOptions> <button type="button" class="btn btn-text-secondary ms-1" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" [disabled]="!field.properties.isContentField && field.isLocked" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>

3
frontend/src/app/features/schemas/pages/schema/schema-page.component.html

@ -41,8 +41,7 @@
</button> </button>
</div> </div>
<button type="button" class="btn btn-text-secondary me-2" data-testid="options" (click)="editOptionsDropdown.toggle()" #buttonOptions> <button type="button" class="btn btn-text-secondary me-2" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="editOptionsDropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </button>

2
frontend/src/app/features/schemas/pages/schemas/schemas-page.component.html

@ -5,7 +5,7 @@
<div class="row g-0"> <div class="row g-0">
<div class="col-auto" *ngIf="schemasState.canCreate | async"> <div class="col-auto" *ngIf="schemasState.canCreate | async">
<button type="button" class="btn btn-success me-2" (click)="createSchema()" <button type="button" class="btn btn-success me-2" (click)="createSchema()"
data-testid="new-schema" attr.aria-label="{{ 'schemas.create' | sqxTranslate }}"
title="i18n:schemas.createSchemaTooltip" title="i18n:schemas.createSchemaTooltip"
titlePosition="top-start" titlePosition="top-start"
shortcut="CTRL + U" shortcut="CTRL + U"

6
frontend/src/app/framework/angular/forms/editable-title.component.html

@ -8,14 +8,12 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button type="submit" class="btn btn-primary btn-{{size}}" data-testid="save" [disabled]="!renameForm.valid || !renameForm.dirty"> <button type="submit" class="btn btn-primary btn-{{size}}" attr.aria-label="{{ 'common.save' | sqxTranslate }}" [disabled]="!renameForm.valid || !renameForm.dirty">
<span class="hidden">{{ 'common.save' | sqxTranslate }}</span>
<i class="icon-checkmark"></i> <i class="icon-checkmark"></i>
</button> </button>
</div> </div>
<div class="col-auto" *ngIf="closeButton"> <div class="col-auto" *ngIf="closeButton">
<button type="button" class="btn btn-text-secondary btn-{{size}} btn-cancel" data-testid="cancel" (click)="toggleRename()"> <button type="button" class="btn btn-text-secondary btn-{{size}} btn-cancel" attr.aria-label="{{ 'common.cancel' | sqxTranslate }}" (click)="toggleRename()">
<span class="hidden">{{ 'common.cancel' | sqxTranslate }}</span>
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</div> </div>

3
frontend/src/app/framework/angular/layout.component.html

@ -49,8 +49,7 @@
</div> </div>
</div> </div>
<button class="btn panel2-collapse" data-testid="back" (click)="toggle()" sqxStopClick> <button class="btn panel2-collapse" attr.aria-label="{{ 'common.collapse' | sqxTranslate }}" (click)="toggle()" sqxStopClick>
<span class="hidden">{{ 'common.back' | sqxTranslate }}</span>
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</button> </button>

4
frontend/src/app/shared/components/contents/content-status.component.html

@ -29,13 +29,13 @@
<ng-template #simple> <ng-template #simple>
<span [class.truncate]="truncate"> <span [class.truncate]="truncate">
<ng-container *ngIf="scheduled; else noSchedule"> <ng-container *ngIf="scheduled; else noSchedule">
<span class="content-status pending" [style.color]="scheduled.color" title="{{tooltipText}}"> <span class="content-status pending" [style.color]="scheduled.color" attr.aria-label="{{tooltipText}}" title="{{tooltipText}}">
<i class="icon-clock" [class.icon-sm]="small"></i> <i class="icon-clock" [class.icon-sm]="small"></i>
</span> </span>
</ng-container> </ng-container>
<ng-template #noSchedule> <ng-template #noSchedule>
<span class="content-status default" [style.color]="statusColor" title="{{tooltipText}}"> <span class="content-status default" [style.color]="statusColor" attr.aria-label="{{tooltipText}}" title="{{tooltipText}}">
<i class="icon-circle" [class.icon-sm]="small"></i> <i class="icon-circle" [class.icon-sm]="small"></i>
</span> </span>
</ng-template> </ng-template>

2
frontend/src/app/shared/components/contents/content-status.component.ts

@ -58,7 +58,7 @@ export class ContentStatusComponent {
} }
public ngOnChanges(changes: TypedSimpleChanges<this>) { public ngOnChanges(changes: TypedSimpleChanges<this>) {
if (changes.scheduled) { if (changes.scheduled || changes.status) {
if (this.scheduled) { if (this.scheduled) {
const args = { status: this.scheduled.status, time: this.scheduled.dueTime.toStringFormat('PPpp') }; const args = { status: this.scheduled.status, time: this.scheduled.dueTime.toStringFormat('PPpp') };

12
frontend/src/app/shared/state/contents.forms.ts

@ -131,9 +131,7 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
}); });
value$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => { value$(this.form).pipe(debounceTimeSafe(debounce), distinctUntilChanged(Types.equals)).subscribe(value => {
this.valueChange$.next(value); this.updateValue(value);
this.updateState(value);
}); });
this.updateInitialData(); this.updateInitialData();
@ -161,6 +159,8 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
if (isInitial) { if (isInitial) {
this.updateInitialData(); this.updateInitialData();
} }
this.updateValue(this.form.value);
} }
protected disable() { protected disable() {
@ -197,6 +197,12 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
} }
} }
private updateValue(value: any) {
this.valueChange$.next(value);
this.updateState(value);
}
private updateInitialData() { private updateInitialData() {
this.initialData = this.form.value; this.initialData = this.form.value;
} }

12
tools/TestSuite/TestSuite.ApiTests/AppClientsTests.cs

@ -70,11 +70,11 @@ public sealed class AppClientsTests : IClassFixture<ClientFixture>
var client_2 = clients_2.Items.Find(x => x.Id == client.Id); var client_2 = clients_2.Items.Find(x => x.Id == client.Id);
// Should update client name. // Should update client name.
Assert.Equal(updateNameRequest.Name, client_2.Name); Assert.Equal(updateNameRequest.Name, client_2?.Name);
Assert.Equal(updateNameRequest.AllowAnonymous, client_2.AllowAnonymous); Assert.Equal(updateNameRequest.AllowAnonymous, client_2?.AllowAnonymous);
Assert.Equal(updateNameRequest.ApiCallsLimit, client_2.ApiCallsLimit); Assert.Equal(updateNameRequest.ApiCallsLimit, client_2?.ApiCallsLimit);
Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2.ApiTrafficLimit); Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2?.ApiTrafficLimit);
Assert.Equal(updateNameRequest.Role, client_2.Role); Assert.Equal(updateNameRequest.Role, client_2?.Role);
await Verify(clients_2) await Verify(clients_2)
.IgnoreMember<ClientDto>(x => x.Secret); .IgnoreMember<ClientDto>(x => x.Secret);
@ -111,6 +111,6 @@ public sealed class AppClientsTests : IClassFixture<ClientFixture>
var clients = await app.Apps.PostClientAsync(createRequest); var clients = await app.Apps.PostClientAsync(createRequest);
var client = clients.Items.Find(x => x.Id == id); var client = clients.Items.Find(x => x.Id == id);
return client; return client!;
} }
} }

46
tools/TestSuite/TestSuite.ApiTests/AppContributorsTests.cs

@ -16,8 +16,6 @@ namespace TestSuite.ApiTests;
[UsesVerify] [UsesVerify]
public sealed class AppContributorsTests : IClassFixture<ClientFixture> public sealed class AppContributorsTests : IClassFixture<ClientFixture>
{ {
private readonly string email = $"{Guid.NewGuid()}@squidex.io";
public ClientFixture _ { get; } public ClientFixture _ { get; }
public AppContributorsTests(ClientFixture fixture) public AppContributorsTests(ClientFixture fixture)
@ -78,7 +76,7 @@ public sealed class AppContributorsTests : IClassFixture<ClientFixture>
// STEP 1: Update contributor role. // STEP 1: Update contributor role.
var updateRequest = new AssignContributorDto var updateRequest = new AssignContributorDto
{ {
ContributorId = email, ContributorId = contributor.ContributorId,
// Test update of role. // Test update of role.
Role = "Owner" Role = "Owner"
}; };
@ -96,30 +94,56 @@ public sealed class AppContributorsTests : IClassFixture<ClientFixture>
var (app, _) = await _.PostAppAsync(); var (app, _) = await _.PostAppAsync();
// STEP 1: Assign contributor. // STEP 1: Assign first contributor.
var contributor = await InviteAsync(app); await InviteAsync(app, "Owner");
// STEP 2: Assign other contributor.
var contributor2 = await InviteAsync(app, "Owner");
// STEP 1: Remove contributor. // STEP 1: Remove contributor.
var contributors_2 = await app.Apps.DeleteContributorAsync(contributor.ContributorId); var contributors_2 = await app.Apps.DeleteContributorAsync(contributor2.ContributorId);
Assert.DoesNotContain(contributors_2.Items, x => x.ContributorId == contributor2.ContributorId);
await Verify(contributors_2)
.IgnoreMember<ContributorDto>(x => x.ContributorId)
.IgnoreMember<ContributorDto>(x => x.ContributorEmail)
.IgnoreMember<ContributorDto>(x => x.ContributorName);
}
[Fact]
public async Task Should_not_remove_single_owner()
{
// STEP 0: Create app.
var (app, _) = await _.PostAppAsync();
Assert.DoesNotContain(contributors_2.Items, x => x.ContributorId == contributor.ContributorId); // STEP 1: Get contributors
var contributor = await InviteAsync(app, "Owner");
await Verify(contributors_2);
// STEP 2: Remove contributor.
await Assert.ThrowsAnyAsync<SquidexException>(() => app.Apps.DeleteContributorAsync(contributor.ContributorId));
} }
private async Task<ContributorDto> InviteAsync(ISquidexClient app) private static async Task<ContributorDto> InviteAsync(ISquidexClient app, string? role = null)
{ {
var email = $"{Guid.NewGuid()}@squidex.io";
var createInviteRequest = new AssignContributorDto var createInviteRequest = new AssignContributorDto
{ {
ContributorId = email, ContributorId = email,
// Invite must be true, otherwise new users are not created. // Invite must be true, otherwise new users are not created.
Invite = true Invite = true,
// The initial role or editor otherwise.
Role = role
}; };
var contributors = await app.Apps.PostContributorAsync(createInviteRequest); var contributors = await app.Apps.PostContributorAsync(createInviteRequest);
var contributor = contributors.Items.Find(x => x.ContributorName == email); var contributor = contributors.Items.Find(x => x.ContributorName == email);
return contributor; return contributor!;
} }
} }

16
tools/TestSuite/TestSuite.ApiTests/AppLanguagesTests.cs

@ -84,8 +84,8 @@ public sealed class AppLanguagesTests : IClassFixture<ClientFixture>
var languages_2 = await app.Apps.PutLanguageAsync("de", updateRequest); var languages_2 = await app.Apps.PutLanguageAsync("de", updateRequest);
var language_2_DE = languages_2.Items.Find(x => x.Iso2Code == "de"); var language_2_DE = languages_2.Items.Find(x => x.Iso2Code == "de");
Assert.Equal(updateRequest.Fallback, language_2_DE.Fallback); Assert.Equal(updateRequest.Fallback, language_2_DE?.Fallback);
Assert.Equal(updateRequest.IsOptional, language_2_DE.IsOptional); Assert.Equal(updateRequest.IsOptional, language_2_DE?.IsOptional);
await Verify(languages_2); await Verify(languages_2);
} }
@ -125,16 +125,16 @@ public sealed class AppLanguagesTests : IClassFixture<ClientFixture>
var language_4_IT = languages_4.Items.Find(x => x.Iso2Code == "it"); var language_4_IT = languages_4.Items.Find(x => x.Iso2Code == "it");
var language_4_EN = languages_4.Items.Find(x => x.Iso2Code == "en"); var language_4_EN = languages_4.Items.Find(x => x.Iso2Code == "en");
Assert.True(language_4_IT.IsMaster); Assert.True(language_4_IT?.IsMaster);
// Old master language is unset. // Old master language is unset.
Assert.False(language_4_EN.IsMaster); Assert.False(language_4_EN?.IsMaster);
// Master language cannot be optional. // Master language cannot be optional.
Assert.False(language_4_IT.IsOptional); Assert.False(language_4_IT?.IsOptional);
// Fallback for new master language must be removed. // Fallback for new master language must be removed.
Assert.Empty(language_4_IT.Fallback); Assert.Empty(language_4_IT?.Fallback!);
await Verify(languages_4); await Verify(languages_4);
} }
@ -169,8 +169,8 @@ public sealed class AppLanguagesTests : IClassFixture<ClientFixture>
var language_2_IT = languages_2.Items.Find(x => x.Iso2Code == "it"); var language_2_IT = languages_2.Items.Find(x => x.Iso2Code == "it");
// Fallback language must be removed. // Fallback language must be removed.
Assert.Empty(language_2_IT.Fallback); Assert.Empty(language_2_IT?.Fallback!);
Assert.Equal(new[] { "en", "it" }, languages_2.Items.Select(x => x.Iso2Code).ToArray()); Assert.Equal(new[] { "en", "it" }, languages_2?.Items.Select(x => x.Iso2Code).ToArray());
await Verify(languages_2); await Verify(languages_2);
} }

16
tools/TestSuite/TestSuite.ApiTests/AppRolesTests.cs

@ -70,7 +70,7 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
var role_2 = roles_2.Items.Find(x => x.Name == roleName); var role_2 = roles_2.Items.Find(x => x.Name == roleName);
// Should return role with correct name. // Should return role with correct name.
Assert.Equal(updateRequest.Permissions, role_2.Permissions); Assert.Equal(updateRequest.Permissions, role_2?.Permissions);
await Verify(role_2) await Verify(role_2)
.IgnoreMember<RoleDto>(x => x.Name); .IgnoreMember<RoleDto>(x => x.Name);
@ -97,8 +97,8 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
var role_2 = roles_2.Items.Find(x => x.Name == roleName); var role_2 = roles_2.Items.Find(x => x.Name == roleName);
// Should return role with correct number of users and clients. // Should return role with correct number of users and clients.
Assert.Equal(1, role_2.NumClients); Assert.Equal(1, role_2?.NumClients);
Assert.Equal(0, role_2.NumContributors); Assert.Equal(0, role_2?.NumContributors);
// STEP 4: Try to delete role. // STEP 4: Try to delete role.
@ -132,8 +132,8 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
var role_2 = roles_2.Items.Find(x => x.Name == roleName); var role_2 = roles_2.Items.Find(x => x.Name == roleName);
// Should return role with correct number of users and clients. // Should return role with correct number of users and clients.
Assert.Equal(0, role_2.NumClients); Assert.Equal(0, role_2?.NumClients);
Assert.Equal(1, role_2.NumContributors); Assert.Equal(1, role_2?.NumContributors);
// STEP 4: Try to delete role.. // STEP 4: Try to delete role..
@ -153,7 +153,7 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName); Assert.DoesNotContain(roles_3.Items, x => x.Name == roleName);
} }
private async Task AssignContributor(string role = null) private async Task AssignContributor(string? role = null)
{ {
var assignRequest = new AssignContributorDto var assignRequest = new AssignContributorDto
{ {
@ -167,7 +167,7 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
await _.Client.Apps.PostContributorAsync(assignRequest); await _.Client.Apps.PostContributorAsync(assignRequest);
} }
private async Task AssignClient(string role = null) private async Task AssignClient(string? role = null)
{ {
var updateRequest = new UpdateClientDto var updateRequest = new UpdateClientDto
{ {
@ -187,6 +187,6 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
var roles = await _.Client.Apps.PostRoleAsync(createRequest); var roles = await _.Client.Apps.PostRoleAsync(createRequest);
var role = roles.Items.Find(x => x.Name == name); var role = roles.Items.Find(x => x.Name == name);
return role; return role!;
} }
} }

2
tools/TestSuite/TestSuite.ApiTests/AppWorkflowsTests.cs

@ -110,6 +110,6 @@ public sealed class AppWorkflowsTests : IClassFixture<ClientFixture>
var workflows = await app.Apps.PostWorkflowAsync(createRequest); var workflows = await app.Apps.PostWorkflowAsync(createRequest);
var workflow = workflows.Items.Find(x => x.Name == workflowName); var workflow = workflows.Items.Find(x => x.Name == workflowName);
return workflow; return workflow!;
} }
} }

19
tools/TestSuite/TestSuite.ApiTests/AssetFoldersTests.cs

@ -75,26 +75,25 @@ public class AssetFoldersTests : IClassFixture<CreatedAppFixture>
Assert.Equal(folder1.Id, folder2_1.ParentId); Assert.Equal(folder1.Id, folder2_1.ParentId);
} }
[Fact] [Theory]
public async Task Should_move_asset() [InlineData(ContentStrategies.Move.Single)]
[InlineData(ContentStrategies.Move.Bulk)]
public async Task Should_move_asset(ContentStrategies.Move strategy)
{ {
// STEP 1: Create folder. // STEP 1: Create folder.
var folder1 = await CreateFolderAsync(Guid.NewGuid().ToString()); var folder1 = await CreateFolderAsync(Guid.NewGuid().ToString());
// STEP 2: Create asset. // STEP 2: Create asset.
var asset_0 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Update folder // STEP 2: Update folder
var moveRequest = new MoveAssetDto await _.Client.Assets.MoveAsync(asset_1, folder1, strategy);
{
ParentId = folder1.Id
};
var asset_1 = await _.Client.Assets.PutAssetParentAsync(asset_0.Id, moveRequest); var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
Assert.Equal(folder1.Id, asset_1.ParentId); Assert.Equal(folder1.Id, asset_2.ParentId);
} }
[Fact] [Fact]
@ -157,7 +156,7 @@ public class AssetFoldersTests : IClassFixture<CreatedAppFixture>
Assert.Contains(folder2.Id, folders3.Items.Select(x => x.Id)); Assert.Contains(folder2.Id, folders3.Items.Select(x => x.Id));
} }
private async Task<AssetFolderDto> CreateFolderAsync(string name, string parentId = null) private async Task<AssetFolderDto> CreateFolderAsync(string name, string? parentId = null)
{ {
var createRequest = new CreateAssetFolderDto var createRequest = new CreateAssetFolderDto
{ {

16
tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs

@ -30,9 +30,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata. // Should parse image metadata.
Assert.True(asset.IsImage); Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth); Assert.Equal(600L, (long)asset.PixelWidth!);
Assert.Equal(600L, asset.Metadata["pixelWidth"]); Assert.Equal(600L, asset.Metadata["pixelWidth"]);
Assert.Equal(400L, (long)asset.PixelHeight); Assert.Equal(400L, (long)asset.PixelHeight!);
Assert.Equal(400L, asset.Metadata["pixelHeight"]); Assert.Equal(400L, asset.Metadata["pixelHeight"]);
Assert.Equal(AssetType.Image, asset.Type); Assert.Equal(AssetType.Image, asset.Type);
@ -119,9 +119,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata. // Should parse image metadata.
Assert.True(asset.IsImage); Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth); Assert.Equal(600L, (long)asset.PixelWidth!);
Assert.Equal(600L, asset.Metadata["pixelWidth"]); Assert.Equal(600L, asset.Metadata["pixelWidth"]);
Assert.Equal(400L, (long)asset.PixelHeight); Assert.Equal(400L, (long)asset.PixelHeight!);
Assert.Equal(400L, asset.Metadata["pixelHeight"]); Assert.Equal(400L, asset.Metadata["pixelHeight"]);
Assert.Equal(AssetType.Image, asset.Type); Assert.Equal(AssetType.Image, asset.Type);
@ -137,9 +137,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata and fix orientation. // Should parse image metadata and fix orientation.
Assert.True(asset.IsImage); Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth); Assert.Equal(600L, (long)asset.PixelWidth!);
Assert.Equal(600L, asset.Metadata["pixelWidth"]); Assert.Equal(600L, asset.Metadata["pixelWidth"]);
Assert.Equal(135L, (long)asset.PixelHeight); Assert.Equal(135L, (long)asset.PixelHeight!);
Assert.Equal(135L, asset.Metadata["pixelHeight"]); Assert.Equal(135L, asset.Metadata["pixelHeight"]);
Assert.Equal(79L, asset.Metadata["imageQuality"]); Assert.Equal(79L, asset.Metadata["imageQuality"]);
Assert.Equal(AssetType.Image, asset.Type); Assert.Equal(AssetType.Image, asset.Type);
@ -232,7 +232,7 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
} }
} }
private async Task<(long, string)> GetReformattedLength(string imageId, string format) private async Task<(long, string?)> GetReformattedLength(string imageId, string format)
{ {
var url = $"{_.Client.GenerateImageUrl(imageId)}?format={format}"; var url = $"{_.Client.GenerateImageUrl(imageId)}?format={format}";
@ -246,7 +246,7 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
await stream.CopyToAsync(buffer); await stream.CopyToAsync(buffer);
return (buffer.Length, response.Content.Headers.ContentType.ToString()); return (buffer.Length, response.Content.Headers.ContentType?.ToString());
} }
} }
} }

151
tools/TestSuite/TestSuite.ApiTests/AssetScriptingTests.cs

@ -0,0 +1,151 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Fixtures;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests;
public class AssetScriptingTests : IClassFixture<ClientFixture>
{
public ClientFixture _ { get; }
public AssetScriptingTests(ClientFixture fixture)
{
_ = fixture;
}
[Fact]
public async Task Should_disallow_jpeg_file_on_create()
{
var (client, _) = await _.PostAppAsync();
// STEP 0: Configure script.
var scriptRequest = new UpdateAssetScriptsDto
{
Create = @"
if (ctx.command.mimeType == 'image/jpg') {
disallow('We do not use jpeg anymore.');
}"
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
// STEP 1: Upload jpeg.
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => client.Assets.UploadFileAsync("Assets/logo-wide-rotated.jpg", "image/jpg"));
Assert.Contains("We do not use jpeg anymore.", ex.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task Should_update_metadata_and_tags_on_create()
{
var (client, _) = await _.PostAppAsync();
// STEP 0: Configure script.
var scriptRequest = new UpdateAssetScriptsDto
{
Create = @"
ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2';
ctx.command.tags.add('tag1');
ctx.command.tags.add('tag2');"
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
// STEP 1: Upload jpeg.
var asset = await client.Assets.UploadFileAsync("Assets/logo-wide-rotated.jpg", "image/jpg");
Assert.Equal("value1", asset.Metadata["key1"]);
Assert.Equal("value2", asset.Metadata["key2"]);
Assert.Contains("tag1", asset.Tags);
Assert.Contains("tag2", asset.Tags);
}
[Fact]
public async Task Should_disallow_jpeg_file_on_update()
{
var (client, _) = await _.PostAppAsync();
// STEP 0: Configure script.
var scriptRequest = new UpdateAssetScriptsDto
{
Update = @"
if (ctx.command.mimeType == 'image/jpg') {
disallow('We do not use jpeg anymore.');
}"
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
// STEP 1. Upload initial png.
var asset_0 = await client.Assets.UploadFileAsync("Assets/logo-wide.png", "image/png");
// STEP 2: Upload jpeg.
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => client.Assets.ReplaceFileAsync(asset_0.Id, "Assets/logo-wide-rotated.jpg", "image/jpg"));
Assert.Contains("We do not use jpeg anymore.", ex.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task Should_update_metadata_on_update()
{
var (client, _) = await _.PostAppAsync();
// STEP 0: Configure script.
var scriptRequest = new UpdateAssetScriptsDto
{
Update = @"
ctx.command.metadata['key1'] = 'value1';
ctx.command.metadata['key2'] = 'value2';"
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
// STEP 1. Upload initial png.
var asset_0 = await client.Assets.UploadFileAsync("Assets/logo-wide.png", "image/png");
// STEP 2: Upload jpeg.
var asset = await client.Assets.ReplaceFileAsync(asset_0.Id, "Assets/logo-wide-rotated.jpg", "image/jpg");
Assert.Equal("value1", asset.Metadata["key1"]);
Assert.Equal("value2", asset.Metadata["key2"]);
}
[Fact]
public async Task Should_query_asset_with_scripting()
{
var (client, _) = await _.PostAppAsync();
// STEP 0: Configure script.
var scriptRequest = new UpdateAssetScriptsDto
{
Query = @"
if (ctx.asset.mimeType == 'image/jpg') {
disallow('We do not use jpeg anymore.');
}"
};
await client.Apps.PutAssetScriptsAsync(scriptRequest);
// STEP 1. Upload initial jpg.
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => client.Assets.UploadFileAsync("Assets/logo-wide-rotated.jpg", "image/jpg"));
Assert.Contains("We do not use jpeg anymore.", ex.ToString(), StringComparison.Ordinal);
}
}

228
tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -38,7 +38,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(asset_1); var downloaded = await _.DownloadAsync(asset_1);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
await Verify(asset_1); await Verify(asset_1);
@ -64,7 +64,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset); var downloaded = await _.DownloadAsync(progress.Asset);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
} }
@ -90,7 +90,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset); var downloaded = await _.DownloadAsync(progress.Asset);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
} }
} }
@ -164,14 +164,14 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
// STEP 2: Reupload asset. // STEP 2: Reupload asset.
var asset_2 = await _.Client.Assets.UploadFileAsync("Assets/logo-wide.png", asset_1); var asset_2 = await _.Client.Assets.ReplaceFileAsync(asset_1.Id, "Assets/logo-wide.png", "image/png");
await using (var stream = new FileStream("Assets/logo-wide.png", FileMode.Open)) await using (var stream = new FileStream("Assets/logo-wide.png", FileMode.Open))
{ {
var downloaded = await _.DownloadAsync(asset_2); var downloaded = await _.DownloadAsync(asset_2);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
await Verify(asset_2); await Verify(asset_2);
@ -201,7 +201,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset); var downloaded = await _.DownloadAsync(progress.Asset);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
} }
@ -231,13 +231,78 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset); var downloaded = await _.DownloadAsync(progress.Asset);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
} }
} }
[Fact] [Fact]
public async Task Should_annote_asset_file_name() public async Task Should_annotate_asset_in_parallel()
{
// STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 3: Make parallel upserts.
await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) =>
{
try
{
var randomTag1 = $"tag_{Guid.NewGuid()}";
var randomTag2 = $"tag_{Guid.NewGuid()}";
var randomMetadataRequest = new AnnotateAssetDto
{
Tags =
[
randomTag1,
randomTag2
]
};
await _.Client.Assets.PutAssetAsync(asset_1.Id, randomMetadataRequest, ct);
}
catch (SquidexException ex) when (ex.StatusCode is 409 or 412)
{
return;
}
});
// STEP 3: Make an normal update to ensure nothing is corrupt.
var tag1 = $"tag_{Guid.NewGuid()}";
var tag2 = $"tag_{Guid.NewGuid()}";
var metadataRequest = new AnnotateAssetDto
{
Tags =
[
tag1,
tag2
]
};
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, metadataRequest);
// STEP 4: Check tags.
var tags = await _.Client.Assets.PollTagAsync(tag1);
Assert.Contains(tag1, tags);
Assert.Contains(tag2, tags);
Assert.Equal(1, tags[tag1]);
Assert.Equal(1, tags[tag2]);
await Verify(asset_2)
.IgnoreMember<AssetDto>(x => x.Version)
.IgnoreMember<AssetDto>(x => x.Tags);
}
[Theory]
[InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_file_name(ContentStrategies.Annotate strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
@ -249,16 +314,20 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
FileName = "My Image" FileName = "My Image"
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, fileNameRequest); await _.Client.Assets.AnnotateAsync(asset_1, fileNameRequest, strategy);
var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
// Should provide updated file name. // Should provide updated file name.
Assert.Equal(fileNameRequest.FileName, asset_2.FileName); Assert.Equal(fileNameRequest.FileName, asset_2.FileName);
await Verify(asset_2); await Verify(asset_2).UseParameters(strategy);
} }
[Fact] [Theory]
public async Task Should_annote_asset_metadata() [InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_metadata(ContentStrategies.Annotate strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
@ -274,16 +343,20 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
} }
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, metadataRequest); await _.Client.Assets.AnnotateAsync(asset_1, metadataRequest, strategy);
var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
// Should provide metadata. // Should provide metadata.
Assert.Equal(metadataRequest.Metadata, asset_2.Metadata); Assert.Equal(metadataRequest.Metadata, asset_2.Metadata);
await Verify(asset_2); await Verify(asset_2).UseParameters(strategy);
} }
[Fact] [Theory]
public async Task Should_annote_asset_slug() [InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_slug(ContentStrategies.Annotate strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
@ -295,16 +368,20 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Slug = "my-image" Slug = "my-image"
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, slugRequest); await _.Client.Assets.AnnotateAsync(asset_1, slugRequest, strategy);
var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
// Should provide updated slug. // Should provide updated slug.
Assert.Equal(slugRequest.Slug, asset_2.Slug); Assert.Equal(slugRequest.Slug, asset_2.Slug);
await Verify(asset_2); await Verify(asset_2).UseParameters(strategy);
} }
[Fact] [Theory]
public async Task Should_annote_asset_tags() [InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_tags(ContentStrategies.Annotate strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
@ -320,79 +397,20 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
] ]
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, tagsRequest); await _.Client.Assets.AnnotateAsync(asset_1, tagsRequest, strategy);
var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
// Should provide updated tags. // Should provide updated tags.
Assert.Equal(tagsRequest.Tags, asset_2.Tags); Assert.Equal(tagsRequest.Tags, asset_2.Tags);
await Verify(asset_2); await Verify(asset_2).UseParameters(strategy);
}
[Fact]
public async Task Should_annotate_asset_in_parallel()
{
// STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 3: Make parallel upserts.
await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) =>
{
try
{
var randomTag1 = $"tag_{Guid.NewGuid()}";
var randomTag2 = $"tag_{Guid.NewGuid()}";
var randomMetadataRequest = new AnnotateAssetDto
{
Tags =
[
randomTag1,
randomTag2
]
};
await _.Client.Assets.PutAssetAsync(asset_1.Id, randomMetadataRequest, ct);
}
catch (SquidexException ex) when (ex.StatusCode is 409 or 412)
{
return;
}
});
// STEP 3: Make an normal update to ensure nothing is corrupt.
var tag1 = $"tag_{Guid.NewGuid()}";
var tag2 = $"tag_{Guid.NewGuid()}";
var metadataRequest = new AnnotateAssetDto
{
Tags =
[
tag1,
tag2
]
};
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, metadataRequest);
// STEP 4: Check tags.
var tags = await _.Client.Assets.PollTagAsync(tag1);
Assert.Contains(tag1, tags);
Assert.Contains(tag2, tags);
Assert.Equal(1, tags[tag1]);
Assert.Equal(1, tags[tag2]);
await Verify(asset_2)
.IgnoreMember<AssetDto>(x => x.Version)
.IgnoreMember<AssetDto>(x => x.Tags);
} }
[Fact] [Theory]
public async Task Should_protect_asset() [InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_protect_asset(ContentStrategies.Annotate strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
@ -404,7 +422,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(asset_1); var downloaded = await _.DownloadAsync(asset_1);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
@ -414,7 +432,9 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
IsProtected = true IsProtected = true
}; };
var asset_2 = await _.Client.Assets.PutAssetAsync(asset_1.Id, protectRequest); await _.Client.Assets.AnnotateAsync(asset_1, protectRequest, strategy);
var asset_2 = await _.Client.Assets.GetAssetAsync(asset_1.Id);
// STEP 5: Download asset with authentication. // STEP 5: Download asset with authentication.
@ -457,7 +477,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode);
} }
await Verify(asset_2); await Verify(asset_2).UseParameters(strategy);
} }
[Fact] [Fact]
@ -486,7 +506,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(asset_1); var downloaded = await _.DownloadAsync(asset_1);
// Should dowload with correct size. // Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length); Assert.Equal(stream.Length, downloaded?.Length);
} }
@ -558,7 +578,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
// STEP 4: Create asset. // STEP 4: Create asset.
var asset_2 = await app.Assets.UpdateFileAsync(asset_1.Id, "Assets/logo-wide.png", "image/png"); var asset_2 = await app.Assets.ReplaceFileAsync(asset_1.Id, "Assets/logo-wide.png", "image/png");
Assert.NotNull(asset_1.Metadata["blurHash"]); Assert.NotNull(asset_1.Metadata["blurHash"]);
Assert.NotNull(asset_2.Metadata["blurHash"]); Assert.NotNull(asset_2.Metadata["blurHash"]);
@ -684,16 +704,18 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
} }
[Theory] [Theory]
[InlineData(false)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(true)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
public async Task Should_delete_asset(bool permanent) [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_asset(ContentStrategies.Deletion strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Delete asset. // STEP 2: Delete asset.
await _.Client.Assets.DeleteAssetAsync(asset.Id, permanent: permanent); await _.Client.Assets.DeleteAsync(asset, strategy);
// Should return 404 when asset deleted. // Should return 404 when asset deleted.
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() => var ex = await Assert.ThrowsAnyAsync<SquidexException>(() =>
@ -705,7 +727,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
// STEP 3: Retrieve all items and ensure that the deleted item does not exist. // STEP 3: Retrieve all items and ensure that the deleted item does not exist.
var updated = await _.Client.Assets.GetAssetsAsync((AssetQuery)null); var updated = await _.Client.Assets.GetAssetsAsync((AssetQuery?)null);
Assert.DoesNotContain(updated.Items, x => x.Id == asset.Id); Assert.DoesNotContain(updated.Items, x => x.Id == asset.Id);
@ -716,20 +738,24 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Filter = "isDeleted eq true" Filter = "isDeleted eq true"
}); });
var permanent = strategy is ContentStrategies.Deletion.SinglePermanent or ContentStrategies.Deletion.BulkPermanent;
Assert.Equal(!permanent, deleted.Items.Exists(x => x.Id == asset.Id)); Assert.Equal(!permanent, deleted.Items.Exists(x => x.Id == asset.Id));
} }
[Theory] [Theory]
[InlineData(false)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(true)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
public async Task Should_recreate_deleted_asset(bool permanent) [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_asset(ContentStrategies.Deletion strategy)
{ {
// STEP 1: Create asset. // STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png"); var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Delete asset. // STEP 2: Delete asset.
await _.Client.Assets.DeleteAssetAsync(asset_1.Id, permanent: permanent); await _.Client.Assets.DeleteAsync(asset_1, strategy);
// STEP 3: Recreate asset. // STEP 3: Recreate asset.

4
tools/TestSuite/TestSuite.ApiTests/BackupTests.cs

@ -51,7 +51,7 @@ public class BackupTests : IClassFixture<ClientFixture>
// STEP 3: Restore backup. // STEP 3: Restore backup.
var uri = new Uri(new Uri(backupUrl), backup.Links["download"].Href); var uri = new Uri(new Uri(backupUrl), backup?.Links["download"].Href);
var restoreRequest = new RestoreRequestDto var restoreRequest = new RestoreRequestDto
{ {
@ -96,7 +96,7 @@ public class BackupTests : IClassFixture<ClientFixture>
// STEP 4: Restore backup. // STEP 4: Restore backup.
var uri = new Uri(new Uri(backupUrl), backup.Links["download"].Href); var uri = new Uri(new Uri(backupUrl), backup?.Links["download"].Href);
var restoreRequest = new RestoreRequestDto var restoreRequest = new RestoreRequestDto
{ {

4
tools/TestSuite/TestSuite.ApiTests/CDNTests.cs

@ -39,7 +39,7 @@ public class CDNTests : IClassFixture<ClientCloudFixture>
var url = _.CDNClient.GenerateImageUrl(id); var url = _.CDNClient.GenerateImageUrl(id);
await DownloadAsync(url); await DownloadAsync(url!);
} }
[Fact] [Fact]
@ -59,7 +59,7 @@ public class CDNTests : IClassFixture<ClientCloudFixture>
var url = _.CloudClient.GenerateImageUrl(id); var url = _.CloudClient.GenerateImageUrl(id);
await DownloadAsync(url); await DownloadAsync(url!);
} }
[Fact] [Fact]

4
tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs

@ -76,7 +76,7 @@ public class ContentCleanupTests : IClassFixture<CreatedAppFixture>
// STEP 3: Create a content with a reference. // STEP 3: Create a content with a reference.
var contentB_1 = await contents.CreateAsync(new TestEntityWithReferencesData var contentB_1 = await contents.CreateAsync(new TestEntityWithReferencesData
{ {
References = new[] { contentA_1.Id } References = [contentA_1.Id]
}); });
@ -91,6 +91,6 @@ public class ContentCleanupTests : IClassFixture<CreatedAppFixture>
}); });
// Should not return deleted field. // Should not return deleted field.
Assert.Empty(contentB_2.Data.References); Assert.Empty(contentB_2.Data.References!);
} }
} }

12
tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs

@ -28,7 +28,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
// STEP 1: Create content. // STEP 1: Create content.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
{ {
Localized = new Dictionary<string, string> Localized = new Dictionary<string, string?>
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello" ["en"] = "Hello"
@ -48,7 +48,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
// STEP 1: Create content. // STEP 1: Create content.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
{ {
Localized = new Dictionary<string, string> Localized = new Dictionary<string, string?>
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello", ["en"] = "Hello",
@ -62,7 +62,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
var contentFlatten = await contents.GetAsync(content.Id, QueryContext.Default.Flatten().WithLanguages(code)); var contentFlatten = await contents.GetAsync(content.Id, QueryContext.Default.Flatten().WithLanguages(code));
Assert.Equal(expected, (string)contentFlatten.Data["localized"]); Assert.Equal(expected, (string?)contentFlatten.Data["localized"]);
} }
[Fact] [Fact]
@ -71,7 +71,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
// STEP 1: Create content. // STEP 1: Create content.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
{ {
Localized = new Dictionary<string, string> Localized = new Dictionary<string, string?>
{ {
["de"] = "Hallo", ["de"] = "Hallo",
["en"] = "Hello" ["en"] = "Hello"
@ -98,7 +98,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
["X-Flatten"] = "1" ["X-Flatten"] = "1"
}); });
static void AssertValue(string value, string not = null) static void AssertValue(string? value, string? not = null)
{ {
Assert.NotNull(value); Assert.NotNull(value);
Assert.NotEmpty(value); Assert.NotEmpty(value);
@ -111,7 +111,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
AssertValue(etag4, etag1); AssertValue(etag4, etag1);
} }
private async Task<(string, string)> GetEtagAsync(string id, Dictionary<string, string> headers) private async Task<(string?, string)> GetEtagAsync(string id, Dictionary<string, string> headers)
{ {
var url = $"{_.Client.Options.Url}api/content/{_.AppName}/{_.SchemaName}/{id}"; var url = $"{_.Client.Options.Url}api/content/{_.AppName}/{_.SchemaName}/{id}";

2
tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs

@ -42,7 +42,7 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase
index + 100, index + 100,
index, index,
oldFormat: index % 2 == 1), oldFormat: index % 2 == 1),
Localized = new Dictionary<string, string> Localized = new Dictionary<string, string?>
{ {
["en"] = index.ToString(CultureInfo.InvariantCulture) ["en"] = index.ToString(CultureInfo.InvariantCulture)
}, },

10
tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -432,7 +432,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
[Fact] [Fact]
public async Task Should_query_json_with_dot() public async Task Should_query_json_with_dot()
{ {
TestEntity content = null; TestEntity? content = null;
try try
{ {
// STEP 1: Create a content item with a text that caused a bug before. // STEP 1: Create a content item with a text that caused a bug before.
@ -467,7 +467,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
var queried = await _.Contents.GetAsync(q); var queried = await _.Contents.GetAsync(q);
Assert.Equal(42, (int)queried.Items[0].Data.Json["search.field.with.dot"]); Assert.Equal(42, (int)queried.Items[0].Data.Json!["search.field.with.dot"]!);
} }
finally finally
{ {
@ -528,7 +528,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); var value = result?["createMyReadsContent"]?["data"]?["number"]?["iv"]?.Value<int>();
Assert.Equal(555, value); Assert.Equal(555, value);
} }
@ -563,7 +563,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>(); var value = result?["createMyReadsContent"]?["data"]?["number"]?["iv"]?.Value<int>();
Assert.Equal(998, value); Assert.Equal(998, value);
} }
@ -695,7 +695,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
var items = result["queryMyReadsContents"]; var items = result["queryMyReadsContents"];
Assert.Equal(new[] { 4, 5, 6 }, items.Select(x => x["data"]["number"]["iv"].Value<int>()).ToArray()); Assert.Equal(new[] { 4, 5, 6 }, items?.Select(x => x["data"]?["number"]?["iv"]?.Value<int>() ?? -1).ToArray());
} }
[Fact] [Fact]

2
tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs

@ -42,7 +42,7 @@ public class ContentReferencesTests : IClassFixture<ContentReferencesFixture>
// STEP 3: Query new item. // STEP 3: Query new item.
var contentB_2 = await _.Contents.GetAsync(contentB_1.Id); var contentB_2 = await _.Contents.GetAsync(contentB_1.Id);
Assert.Empty(contentB_2.Data.References); Assert.Empty(contentB_2.Data.References!);
// STEP 4: Publish reference. // STEP 4: Publish reference.

6
tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs

@ -76,7 +76,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
} }
[Fact] [Fact]
public async Task Should_query_content_with_scripting_and_pre_query() public async Task Should_query_content_with_scripting_and_prepare_script()
{ {
var scripts = new SchemaScriptsDto var scripts = new SchemaScriptsDto
{ {
@ -151,7 +151,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
} }
[Fact] [Fact]
public async Task Should_create_bulk_content_with_scripting_but_disabled() public async Task Should_create_bulk_content_with_disabled_scripting()
{ {
// STEP 1: Create a schema. // STEP 1: Create a schema.
var scripts = new SchemaScriptsDto var scripts = new SchemaScriptsDto
@ -183,7 +183,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
iv = 99 iv = 99
} }
} }
} },
], ],
Publish = true Publish = true
}); });

128
tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -125,7 +125,7 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
// STEP 1: Create a content item with a text that caused a bug before. // STEP 1: Create a content item with a text that caused a bug before.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
{ {
Localized = new Dictionary<string, string> Localized = new Dictionary<string, string?>
{ {
["en"] = null ["en"] = null
} }
@ -156,7 +156,7 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
// STEP 2: Get the item and ensure that the text is the same. // STEP 2: Get the item and ensure that the text is the same.
var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback()); var queried = await _.Contents.GetAsync(content.Id, QueryContext.Default.IgnoreFallback());
Assert.Equal(42, (int)queried.Data.Json["field.with.dot"]); Assert.Equal(42, (int)queried.Data.Json!["field.with.dot"]!);
await Verify(queried); await Verify(queried);
} }
@ -331,13 +331,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Update.Normal)] [InlineData(ContentStrategies.Update.Normal)]
[InlineData(Strategies.Update.Upsert)] [InlineData(ContentStrategies.Update.Upsert)]
[InlineData(Strategies.Update.UpsertBulk)] [InlineData(ContentStrategies.Update.UpsertBulk)]
[InlineData(Strategies.Update.Bulk)] [InlineData(ContentStrategies.Update.Bulk)]
[InlineData(Strategies.Update.BulkShared)] [InlineData(ContentStrategies.Update.BulkShared)]
[InlineData(Strategies.Update.BulkWithSchema)] [InlineData(ContentStrategies.Update.BulkWithSchema)]
public async Task Should_update_content(Strategies.Update strategy) public async Task Should_update_content(ContentStrategies.Update strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -361,13 +361,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Update.Normal)] [InlineData(ContentStrategies.Update.Normal)]
[InlineData(Strategies.Update.Upsert)] [InlineData(ContentStrategies.Update.Upsert)]
[InlineData(Strategies.Update.UpsertBulk)] [InlineData(ContentStrategies.Update.UpsertBulk)]
[InlineData(Strategies.Update.Bulk)] [InlineData(ContentStrategies.Update.Bulk)]
[InlineData(Strategies.Update.BulkShared)] [InlineData(ContentStrategies.Update.BulkShared)]
[InlineData(Strategies.Update.BulkWithSchema)] [InlineData(ContentStrategies.Update.BulkWithSchema)]
public async Task Should_update_content_to_null(Strategies.Update strategy) public async Task Should_update_content_to_null(ContentStrategies.Update strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -388,13 +388,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Patch.Normal)] [InlineData(ContentStrategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)] [InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)] [InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)] [InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)] [InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)] [InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_content(Strategies.Patch strategy) public async Task Should_patch_content(ContentStrategies.Patch strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -418,13 +418,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Patch.Normal)] [InlineData(ContentStrategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)] [InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)] [InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)] [InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)] [InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)] [InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_id_data_value(Strategies.Patch strategy) public async Task Should_patch_id_data_value(ContentStrategies.Patch strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -445,13 +445,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Patch.Normal)] [InlineData(ContentStrategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)] [InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)] [InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)] [InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)] [InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)] [InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_content_to_null(Strategies.Patch strategy) public async Task Should_patch_content_to_null(ContentStrategies.Patch strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -463,7 +463,7 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
// STEP 2: Patch with selected strategy. // STEP 2: Patch with selected strategy.
await _.Client.PatchAsync(content, new await _.Client.PatchAsync(content, new
{ {
@string = new { iv = (string)null } @string = new { iv = (string?)null }
}, strategy); }, strategy);
var updated = await _.Contents.GetAsync(content.Id); var updated = await _.Contents.GetAsync(content.Id);
@ -472,11 +472,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Deletion.SingleSoft)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)] [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)] [InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_content(Strategies.Deletion strategy) public async Task Should_delete_content(ContentStrategies.Deletion strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData var content = await _.Contents.CreateAsync(new TestEntityData
@ -500,15 +500,15 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
var deleted = await _.Contents.GetAsync(q, QueryContext.Default.Unpublished(true)); var deleted = await _.Contents.GetAsync(q, QueryContext.Default.Unpublished(true));
Assert.Equal(strategy is Strategies.Deletion.SingleSoft or Strategies.Deletion.BulkSoft, deleted.Items.Exists(x => x.Id == content.Id)); Assert.Equal(strategy is ContentStrategies.Deletion.SingleSoft or ContentStrategies.Deletion.BulkSoft, deleted.Items.Exists(x => x.Id == content.Id));
} }
[Theory] [Theory]
[InlineData(Strategies.Deletion.SingleSoft)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)] [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)] [InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_create_content_with_custom_id_and_delete_it(Strategies.Deletion strategy) public async Task Should_create_content_with_custom_id_and_delete_it(ContentStrategies.Deletion strategy)
{ {
var id = $"custom-{Guid.NewGuid()}"; var id = $"custom-{Guid.NewGuid()}";
@ -534,11 +534,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Deletion.SingleSoft)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)] [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)] [InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content(Strategies.Deletion strategy) public async Task Should_recreate_deleted_content(ContentStrategies.Deletion strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData var content_1 = await _.Contents.CreateAsync(new TestEntityData
@ -571,11 +571,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Deletion.SingleSoft)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)] [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)] [InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content_with_upsert(Strategies.Deletion strategy) public async Task Should_recreate_deleted_content_with_upsert(ContentStrategies.Deletion strategy)
{ {
// STEP 1: Create a new item. // STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData var content_1 = await _.Contents.CreateAsync(new TestEntityData
@ -598,11 +598,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
} }
[Theory] [Theory]
[InlineData(Strategies.Deletion.SingleSoft)] [InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)] [InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)] [InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)] [InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_recreated_content(Strategies.Deletion strategy) public async Task Should_delete_recreated_content(ContentStrategies.Deletion strategy)
{ {
var id = $"custom-{Guid.NewGuid()}"; var id = $"custom-{Guid.NewGuid()}";

2
tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs

@ -138,7 +138,7 @@ public sealed class GraphQLFixture : ContentFixture
{ {
SchemaIds = [citiesId] SchemaIds = [citiesId]
} }
} },
], ],
IsPublished = true IsPublished = true
}; };

2
tools/TestSuite/TestSuite.ApiTests/GraphQLSubscriptionTests.cs

@ -130,7 +130,7 @@ public class GraphQLSubscriptionTests : IClassFixture<ContentFixture>
var options = new GraphQLHttpClientOptions var options = new GraphQLHttpClientOptions
{ {
EndPoint = new Uri(_.Client.GenerateUrl($"/api/content/{_.AppName}/graphql?access_token={accessToken}")) EndPoint = new Uri(_.Client.GenerateUrl($"/api/content/{_.AppName}/graphql?access_token={accessToken}")!)
}; };
var client = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer()); var client = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer());

50
tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -62,8 +62,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
Assert.Equal(1, result["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>()); Assert.Equal(1, result?["findMyWritesContent"]?["flatData"]?["json"]?["value"]?.Value<int>());
Assert.Equal(2, result["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>()); Assert.Equal(2, result?["findMyWritesContent"]?["flatData"]?["json"]?["obj"]?["value"]?.Value<int>());
} }
[Fact] [Fact]
@ -89,7 +89,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cities = result["cities"].ToObject<List<City>>(); var cities = result?["cities"]?.ToObject<List<City>>();
cities.Should().BeEquivalentTo(new List<City> cities.Should().BeEquivalentTo(new List<City>
{ {
@ -168,7 +168,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames = var cityNames =
result["countries"].ToObject<List<Country>>()[0].Data.States result!["countries"]!.ToObject<List<Country>>()![0].Data.States
.SelectMany(x => x.Data.Cities) .SelectMany(x => x.Data.Cities)
.Select(x => x.Data.Name) .Select(x => x.Data.Name)
.Order(); .Order();
@ -204,10 +204,10 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames = var cityNames =
result["countries"] result!["countries"]!
.SelectMany(x => x["data"]["states"]) .SelectMany(x => x["data"]!["states"]!)
.SelectMany(x => x["cities"]) .SelectMany(x => x["cities"]!)
.Select(x => x["data"]["name"].Value<string>()) .Select(x => x["data"]!["name"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames);
@ -241,10 +241,10 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames = var cityNames =
result["countries"] result["countries"]!
.SelectMany(x => x["data"]["states"]) .SelectMany(x => x["data"]!["states"]!)
.SelectMany(x => x["cities"]) .SelectMany(x => x["cities"]!)
.Select(x => x["data"]["name"].Value<string>()) .Select(x => x["data"]!["name"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Leipzig" }, cityNames); Assert.Equal(new[] { "Leipzig" }, cityNames);
@ -270,9 +270,9 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var stateNames = var stateNames =
result["cities"] result["cities"]!
.SelectMany(x => x["states"]) .SelectMany(x => x["states"]!)
.Select(x => x["data"]["name"].Value<string>()) .Select(x => x["data"]!["name"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Bavaria", "Saxony" }, stateNames); Assert.Equal(new[] { "Bavaria", "Saxony" }, stateNames);
@ -298,9 +298,9 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var stateNames = var stateNames =
result["cities"] result!["cities"]!
.SelectMany(x => x["states"]) .SelectMany(x => x!["states"]!)
.Select(x => x["data"]["name"].Value<string>()) .Select(x => x!["data"]!["name"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Saxony" }, stateNames); Assert.Equal(new[] { "Saxony" }, stateNames);
@ -322,8 +322,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames = var cityNames =
result["cities"] result["cities"]!
.Select(x => x["data__dynamic"]["name"]["iv"].Value<string>()) .Select(x => x["data__dynamic"]!["name"]!["iv"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames); Assert.Equal(new[] { "Leipzig", "Munich" }, cityNames);
@ -347,7 +347,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
// Create the request manually to check the content type. // Create the request manually to check the content type.
var response = await httpClient.PostAsync(_.Client.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); var response = await httpClient.PostAsync(_.Client.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent());
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType);
} }
[Fact] [Fact]
@ -387,8 +387,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var names = var names =
result["queryContentsByIds"] result["queryContentsByIds"]!
.Select(x => x["data"]["name"].Value<string>()) .Select(x => x["data"]!["name"]!.Value<string>())
.Order(); .Order();
Assert.Equal(new[] { "Bavaria", "Leipzig", "Munich", "Saxony" }, names); Assert.Equal(new[] { "Bavaria", "Leipzig", "Munich", "Saxony" }, names);
@ -425,8 +425,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query); var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var city1Id = result["a"]["id"].ToString(); var city1Id = result!["a"]!["id"]!.ToString();
var city2Id = result["b"]["id"].ToString(); var city2Id = result!["b"]!["id"]!.ToString();
Assert.Equal(allCities.Items[0].Id, city1Id); Assert.Equal(allCities.Items[0].Id, city1Id);
Assert.Equal(allCities.Items[1].Id, city2Id); Assert.Equal(allCities.Items[1].Id, city2Id);

16
tools/TestSuite/TestSuite.ApiTests/RuleRunnerTests.cs

@ -106,10 +106,10 @@ public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<Webho
await parentContents.CreateAsync(new TestEntityWithReferencesData await parentContents.CreateAsync(new TestEntityWithReferencesData
{ {
References = new[] References =
{ [
referencedContent.Id referencedContent.Id
} ],
}); });
@ -138,14 +138,14 @@ public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<Webho
new SchemaCondition new SchemaCondition
{ {
SchemaId = parentSchema.Id SchemaId = parentSchema.Id
} },
], ],
ReferencedSchemas = ReferencedSchemas =
[ [
new SchemaCondition new SchemaCondition
{ {
SchemaId = referencedSchema.Id SchemaId = referencedSchema.Id
} },
] ]
} }
}; };
@ -464,11 +464,11 @@ public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<Webho
} }
} }
private void AssertRequest(WebhookRequest request) private void AssertRequest(WebhookRequest? request)
{ {
Assert.NotNull(request); Assert.NotNull(request);
Assert.NotNull(request.Headers["X-Signature"]); Assert.NotNull(request?.Headers["X-Signature"]);
Assert.Equal(request.Headers["X-Signature"], WebhookUtils.CalculateSignature(request.Content, secret)); Assert.Equal(request?.Headers["X-Signature"], WebhookUtils.CalculateSignature(request?.Content ?? string.Empty, secret));
} }
} }

128
tools/TestSuite/TestSuite.ApiTests/TeamContributorTests.cs

@ -0,0 +1,128 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Fixtures;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests;
[UsesVerify]
public class TeamContributorTests : IClassFixture<ClientFixture>
{
public ClientFixture _ { get; }
public TeamContributorTests(ClientFixture fixture)
{
_ = fixture;
}
[Fact]
public async Task Should_not_invite_contributor_if_flag_is_false()
{
// STEP 0: Create team.
var team = await _.PostTeamAsync();
// STEP 1: Do not invite contributors when flag is false.
var createRequest = new AssignContributorDto
{
ContributorId = "test@squidex.io"
};
var ex = await Assert.ThrowsAnyAsync<SquidexException>(() =>
{
return _.Client.Teams.PostContributorAsync(team.Id, createRequest);
});
Assert.Equal(404, ex.StatusCode);
}
[Fact]
public async Task Should_invite_contributor()
{
// STEP 0: Create team.
var team = await _.PostTeamAsync();
// STEP 0: Create app.
var (app, _) = await _.PostAppAsync();
// STEP 1: Assign contributor.
ContributorDto contributor_1 = await InviteAsync(team.Id);
Assert.Equal("Owner", contributor_1?.Role);
await Verify(contributor_1)
.IgnoreMember<ContributorDto>(x => x.ContributorId)
.IgnoreMember<ContributorDto>(x => x.ContributorEmail)
.IgnoreMember<ContributorDto>(x => x.ContributorName);
}
[Fact]
public async Task Should_remove_contributor()
{
// STEP 0: Create team.
var team = await _.PostTeamAsync();
// STEP 1: Assign first contributor.
await InviteAsync(team.Id);
// STEP 2: Assign other contributor.
var contributor2 = await InviteAsync(team.Id);
// STEP 2: Remove contributor.
var contributors_2 = await _.Client.Teams.DeleteContributorAsync(team.Id, contributor2.ContributorId);
Assert.DoesNotContain(contributors_2.Items, x => x.ContributorId == contributor2.ContributorId);
await Verify(contributors_2)
.IgnoreMember<ContributorDto>(x => x.ContributorId)
.IgnoreMember<ContributorDto>(x => x.ContributorEmail)
.IgnoreMember<ContributorDto>(x => x.ContributorName);
}
[Fact]
public async Task Should_not_remove_single_owner()
{
// STEP 0: Create team.
var team = await _.PostTeamAsync();
// STEP 1: Get contributors
var contributor = await InviteAsync(team.Id);
// STEP 2: Remove contributor.
await Assert.ThrowsAnyAsync<SquidexException>(() => _.Client.Teams.DeleteContributorAsync(team.Id, contributor.ContributorId));
}
private async Task<ContributorDto> InviteAsync(string teamId)
{
var email = $"{Guid.NewGuid()}@squidex.io";
var createInviteRequest = new AssignContributorDto
{
ContributorId = email,
// Invite must be true, otherwise new users are not created.
Invite = true,
// This is the only allowed role for teams.
Role = "Owner"
};
var contributors = await _.Client.Teams.PostContributorAsync(teamId, createInviteRequest);
var contributor = contributors.Items.Find(x => x.ContributorName == email);
return contributor!;
}
}

31
tools/TestSuite/TestSuite.ApiTests/TeamCreationTests.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Fixtures; using TestSuite.Fixtures;
#pragma warning disable SA1300 // Element should begin with upper-case letter #pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests; namespace TestSuite.ApiTests;
@ -26,12 +26,7 @@ public class TeamCreationTests : IClassFixture<ClientFixture>
[Fact] [Fact]
public async Task Should_create_team() public async Task Should_create_team()
{ {
var request = new CreateTeamDto var team = await _.PostTeamAsync(teamName);
{
Name = teamName
};
var team = await _.Client.Teams.PostTeamAsync(request);
Assert.Equal(teamName, team.Name); Assert.Equal(teamName, team.Name);
} }
@ -39,16 +34,24 @@ public class TeamCreationTests : IClassFixture<ClientFixture>
[Fact] [Fact]
public async Task Should_create_team_with_duplicate_name() public async Task Should_create_team_with_duplicate_name()
{ {
var request = new CreateTeamDto var team1 = await _.PostTeamAsync(teamName);
{ var team2 = await _.PostTeamAsync(teamName);
Name = teamName
};
var team1 = await _.Client.Teams.PostTeamAsync(request);
var team2 = await _.Client.Teams.PostTeamAsync(request);
Assert.Equal(teamName, team1.Name); Assert.Equal(teamName, team1.Name);
Assert.Equal(teamName, team2.Name); Assert.Equal(teamName, team2.Name);
Assert.NotEqual(team1.Id, team2.Id); Assert.NotEqual(team1.Id, team2.Id);
} }
[Fact]
public async Task Should_get_team_by_id()
{
// STEP 0: Create team.
var team_0 = await _.PostTeamAsync(teamName);
// STEP 2: Get team.
var team_1 = await _.Client.Teams.GetTeamAsync(team_0.Id);
Assert.Equal(team_0.Id, team_1.Id);
}
} }

86
tools/TestSuite/TestSuite.ApiTests/TeamTests.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.ClientLibrary;
using TestSuite.Fixtures;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
namespace TestSuite.ApiTests;
[UsesVerify]
public class TeamTests : IClassFixture<CreatedTeamFixture>
{
public CreatedTeamFixture _ { get; }
public TeamTests(CreatedTeamFixture fixture)
{
_ = fixture;
}
[Fact]
public async Task Should_set_name()
{
// STEP 1: Update app.
var updateRequest = new UpdateTeamDto
{
Name = Guid.NewGuid().ToString()
};
var app_1 = await _.Client.Teams.PutTeamAsync(_.TeamId, updateRequest);
Assert.Equal(updateRequest.Name, app_1.Name);
}
[Fact]
public async Task Should_transfer_app_to_team()
{
// STEP 0: Create app.
var (client, _) = await _.PostAppAsync();
// STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto
{
TeamId = _.TeamId
};
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest);
Assert.Equal(_.TeamId, app_1.TeamId);
}
[Fact]
public async Task Should_remove_app_from_team()
{
// STEP 0: Create app.
var (client, _) = await _.PostAppAsync();
// STEP 1: Assign app to team.
var transferRequest = new TransferToTeamDto
{
TeamId = _.TeamId
};
var app_1 = await client.Apps.PutAppTeamAsync(transferRequest);
Assert.Equal(_.TeamId, app_1.TeamId);
// STEP 2: Remove app from team.
var untransferRequest = new TransferToTeamDto
{
TeamId = null
};
var app_2 = await client.Apps.PutAppTeamAsync(untransferRequest);
Assert.Null(app_2.TeamId);
}
}

1
tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Codeuctivity.ImageSharpCompare" Version="4.0.204" /> <PackageReference Include="Codeuctivity.ImageSharpCompare" Version="4.0.204" />

13
tools/TestSuite/TestSuite.ApiTests/Verify/AppContributorsTests.Should_remove_contributor.verified.txt

@ -1,4 +1,17 @@
{ {
Items: [
{
Role: Owner,
Links: {
delete: {
Method: DELETE
},
update: {
Method: POST
}
}
}
],
MaxContributors: -1, MaxContributors: -1,
Links: { Links: {
create: { create: {

0
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_file_name.verified.txt → tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_file_name_strategy=Bulk.verified.txt

47
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_file_name_strategy=Single.verified.txt

@ -0,0 +1,47 @@
{
Id: Guid_1,
ParentId: Guid_Empty,
FileName: My Image,
FileHash: pAp0RDvipkoNCCTgey1HTJekRKKEWT6Ft5JdRLuAfAc=,
IsProtected: false,
Slug: logo-squared.png,
MimeType: image/png,
FileType: blob,
MetadataText: 600x600px, 19 kB,
Metadata: {
description: PNG File,
pixelHeight: 600,
pixelWidth: 600
},
Tags: [
type/png,
image,
image/medium
],
FileSize: 19430,
Type: Image,
Version: 1,
Links: {
content: {
Method: GET
},
content/slug: {
Method: GET
},
delete: {
Method: DELETE
},
move: {
Method: PUT
},
self: {
Method: GET
},
update: {
Method: PUT
},
upload: {
Method: PUT
}
}
}

0
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_metadata.verified.txt → tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_metadata_strategy=Bulk.verified.txt

46
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_metadata_strategy=Single.verified.txt

@ -0,0 +1,46 @@
{
Id: Guid_1,
ParentId: Guid_Empty,
FileName: logo-squared.png,
FileHash: pAp0RDvipkoNCCTgey1HTJekRKKEWT6Ft5JdRLuAfAc=,
IsProtected: false,
Slug: logo-squared.png,
MimeType: image/png,
FileType: png,
MetadataText: 19 kB,
Metadata: {
ph: 20,
pw: 100
},
Tags: [
type/png,
image,
image/medium
],
FileSize: 19430,
Type: Image,
Version: 1,
Links: {
content: {
Method: GET
},
content/slug: {
Method: GET
},
delete: {
Method: DELETE
},
move: {
Method: PUT
},
self: {
Method: GET
},
update: {
Method: PUT
},
upload: {
Method: PUT
}
}
}

0
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_slug.verified.txt → tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_slug_strategy=Bulk.verified.txt

47
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_slug_strategy=Single.verified.txt

@ -0,0 +1,47 @@
{
Id: Guid_1,
ParentId: Guid_Empty,
FileName: logo-squared.png,
FileHash: pAp0RDvipkoNCCTgey1HTJekRKKEWT6Ft5JdRLuAfAc=,
IsProtected: false,
Slug: my-image,
MimeType: image/png,
FileType: png,
MetadataText: 600x600px, 19 kB,
Metadata: {
description: PNG File,
pixelHeight: 600,
pixelWidth: 600
},
Tags: [
type/png,
image,
image/medium
],
FileSize: 19430,
Type: Image,
Version: 1,
Links: {
content: {
Method: GET
},
content/slug: {
Method: GET
},
delete: {
Method: DELETE
},
move: {
Method: PUT
},
self: {
Method: GET
},
update: {
Method: PUT
},
upload: {
Method: PUT
}
}
}

0
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_tags.verified.txt → tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_tags_strategy=Bulk.verified.txt

46
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_annote_asset_tags_strategy=Single.verified.txt

@ -0,0 +1,46 @@
{
Id: Guid_1,
ParentId: Guid_Empty,
FileName: logo-squared.png,
FileHash: pAp0RDvipkoNCCTgey1HTJekRKKEWT6Ft5JdRLuAfAc=,
IsProtected: false,
Slug: logo-squared.png,
MimeType: image/png,
FileType: png,
MetadataText: 600x600px, 19 kB,
Metadata: {
description: PNG File,
pixelHeight: 600,
pixelWidth: 600
},
Tags: [
tag1,
tag2
],
FileSize: 19430,
Type: Image,
Version: 1,
Links: {
content: {
Method: GET
},
content/slug: {
Method: GET
},
delete: {
Method: DELETE
},
move: {
Method: PUT
},
self: {
Method: GET
},
update: {
Method: PUT
},
upload: {
Method: PUT
}
}
}

0
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_protect_asset.verified.txt → tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_protect_asset_strategy=Bulk.verified.txt

47
tools/TestSuite/TestSuite.ApiTests/Verify/AssetTests.Should_protect_asset_strategy=Single.verified.txt

@ -0,0 +1,47 @@
{
Id: Guid_1,
ParentId: Guid_Empty,
FileName: logo-squared.png,
FileHash: pAp0RDvipkoNCCTgey1HTJekRKKEWT6Ft5JdRLuAfAc=,
IsProtected: true,
Slug: logo-squared.png,
MimeType: image/png,
FileType: png,
MetadataText: 600x600px, 19 kB,
Metadata: {
description: PNG File,
pixelHeight: 600,
pixelWidth: 600
},
Tags: [
type/png,
image,
image/medium
],
FileSize: 19430,
Type: Image,
Version: 1,
Links: {
content: {
Method: GET
},
content/slug: {
Method: GET
},
delete: {
Method: DELETE
},
move: {
Method: PUT
},
self: {
Method: GET
},
update: {
Method: PUT
},
upload: {
Method: PUT
}
}
}

11
tools/TestSuite/TestSuite.ApiTests/Verify/TeamContributorTests.Should_invite_contributor.verified.txt

@ -0,0 +1,11 @@
{
Role: Owner,
Links: {
delete: {
Method: DELETE
},
update: {
Method: POST
}
}
}

23
tools/TestSuite/TestSuite.ApiTests/Verify/TeamContributorTests.Should_remove_contributor.verified.txt

@ -0,0 +1,23 @@
{
Items: [
{
Role: Owner,
Links: {
delete: {
Method: DELETE
},
update: {
Method: POST
}
}
}
],
Links: {
create: {
Method: POST
},
self: {
Method: GET
}
}
}

2
tools/TestSuite/TestSuite.LoadTests/Run.cs

@ -14,7 +14,7 @@ namespace TestSuite.LoadTests;
public static class Run public static class Run
{ {
public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100, ITestOutputHelper testOutput = null) public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100, ITestOutputHelper? testOutput = null)
{ {
await action(); await action();

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save