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
internal partial class DeepDetectActionHandler : RuleActionHandler<DeepDetectAction, DeepDetectJob>
internal sealed partial class DeepDetectActionHandler : RuleActionHandler<DeepDetectAction, DeepDetectJob>
{
private const string Description = "Analyze Image";
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;
internal class DeepDetectPlugin : IPlugin
internal sealed class DeepDetectPlugin : IPlugin
{
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())
{
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();

1
backend/i18n/frontend_en.json

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

1
backend/i18n/frontend_fr.json

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

1
backend/i18n/frontend_it.json

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

1
backend/i18n/frontend_nl.json

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

1
backend/i18n/frontend_pt.json

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

1
backend/i18n/frontend_zh.json

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

1
backend/i18n/source/frontend_en.json

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

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

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

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

@ -35,17 +35,17 @@ public sealed class ContentChangedTriggerSchema
if (SendCreate)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Created}'");
conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Created)}'");
}
if (SendUpdate)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Updated}'");
conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Updated)}'");
}
if (SendPublish)
{
conditions.Add($"event.type == '{EnrichedContentEventType.Published}'");
conditions.Add($"event.type == '{nameof(EnrichedContentEventType.Published)}'");
}
if (SendArchived)
@ -55,7 +55,7 @@ public sealed class ContentChangedTriggerSchema
if (SendDelete)
{
conditions.Add($"event.type == '{EnrichedAssetEventType.Deleted}'");
conditions.Add($"event.type == '{nameof(EnrichedAssetEventType.Deleted)}'");
}
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>
/// Looks up a localized string similar to The mime type..
/// </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>
/// Looks up a localized string similar to The current operation..
/// </summary>

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

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

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

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Scripting;
@ -51,15 +51,15 @@ public sealed class AssetEntityScriptVars : ScriptVars
}
[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))]
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))]

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();
fieldsToDelete ??= new HashSet<string>();
fieldsToDelete ??= [];
fieldsToDelete.Add(propertyName);
fieldProperties?.Remove(propertyName);
@ -114,7 +114,7 @@ public sealed class ContentDataObject : ObjectInstance
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()

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();
contentField = new ContentFieldObject(contentData, new ContentFieldData(), true);
contentField = new ContentFieldObject(contentData, [], true);
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();
valuesToDelete ??= new HashSet<string>();
valuesToDelete ??= [];
valuesToDelete.Add(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.Text.RegularExpressions;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
@ -116,7 +115,7 @@ public sealed partial class ScriptingCompleter
{
private static readonly Regex RegexProperty = BuildPropertyRegex();
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 FilterSchema? dataSchema;
@ -280,7 +279,7 @@ public sealed partial class ScriptingCompleter
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);
}
@ -304,14 +303,6 @@ public sealed partial class ScriptingCompleter
{
AddFunction(name, description);
}
else if (propertyType == typeof(AssetMetadata))
{
AddObject(name, description, () =>
{
AddString("my-name",
FieldDescriptions.AssetMetadataValue);
});
}
else if (propertyType == typeof(NamedId<DomainId>))
{
AddObject(name, description, () =>
@ -371,13 +362,31 @@ public sealed partial class ScriptingCompleter
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);
}
}
}
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)
{
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
});
if (upsert.GeoObjects?.Any() == true)
if (upsert.GeoObjects?.Count > 0)
{
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.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards;
@ -62,11 +63,11 @@ public static class ScriptingExtensions
FileSize = create.File.FileSize,
FileVersion = 0,
IsProtected = false,
Metadata = create.Metadata,
Metadata = create.Metadata?.ToReadonlyDictionary(),
MimeType = create.File.MimeType,
ParentId = create.ParentId,
ParentPath = await GetPathAsync(operation, create.ParentId, ct),
Tags = create.Tags
Tags = create.Tags?.ToReadonlyList()
};
await ExecuteScriptAsync(operation, script, vars, asset, ct);
@ -194,11 +195,11 @@ public static class ScriptingExtensions
FileSlug = snapshot.Slug,
FileVersion = snapshot.FileVersion,
IsProtected = snapshot.IsProtected,
Metadata = snapshot.Metadata,
Metadata = snapshot.Metadata?.ToReadonlyDictionary(),
MimeType = snapshot.MimeType,
ParentId = snapshot.ParentId,
ParentPath = await GetPathAsync(operation, snapshot.ParentId, ct),
Tags = snapshot.Tags,
Tags = snapshot.Tags?.ToReadonlyList(),
};
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.Infrastructure.Collections;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Assets.Queries.Steps;
@ -76,11 +77,11 @@ public sealed class ScriptAsset : IAssetEnricherStep
FileSlug = asset.Slug,
FileVersion = asset.FileVersion,
IsProtected = asset.IsProtected,
Metadata = asset.Metadata,
Metadata = asset.Metadata?.ToReadonlyDictionary(),
MimeType = asset.MimeType,
ParentId = asset.ParentId,
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;
}
public EmailUserNotifications(
IOptions<EmailUserNotificationOptions> texts,
IEmailSender emailSender,
IUrlGenerator urlGenerator,
ILogger<EmailUserNotifications> log)
public EmailUserNotifications(IOptions<EmailUserNotificationOptions> texts, IEmailSender emailSender,
IUrlGenerator urlGenerator, ILogger<EmailUserNotifications> log)
{
this.texts = texts.Value;
this.emailSender = emailSender;

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

@ -54,7 +54,4 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Notifications\" />
</ItemGroup>
</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);
}
if (!smtpClient.IsAuthenticated)
if (!smtpClient.IsAuthenticated && options.IsAuthenticating())
{
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
!string.IsNullOrWhiteSpace(Server) &&
!string.IsNullOrWhiteSpace(Sender) &&
!string.IsNullOrWhiteSpace(Sender);
}
public bool IsAuthenticating()
{
return
!string.IsNullOrWhiteSpace(Username) &&
!string.IsNullOrWhiteSpace(Password);
}

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

@ -202,6 +202,15 @@ public static class HashExtensions
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)
{
string result;
@ -213,7 +222,7 @@ public static class HashExtensions
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
result = ConvertCore(data, buffer);
result = ConvertCore(data, buffer, 0);
}
finally
{
@ -224,29 +233,63 @@ public static class HashExtensions
{
Span<char> buffer = stackalloc char[length];
result = ConvertCore(data, buffer);
result = ConvertCore(data, buffer, 0);
}
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;
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
buffer[0] = '"';
buffer[^1] = '"';
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));
}
result = ConvertCore(data, buffer, 1);
}
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.Net.Http.Headers;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Http;
using Squidex.Domain.Apps.Entities;
@ -60,21 +61,18 @@ public static class ETagExtensions
{
version = default;
if (httpContext.Request.Headers.TryGetString(header, out var etag))
if (!httpContext.Request.Headers.TryGetValue(header, out var etagValue))
{
var span = etag.AsSpan();
if (span.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
span = span[2..];
}
return false;
}
if (long.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture, out version))
{
return true;
}
if (!EntityTagHeaderValue.TryParse(etagValue.ToString(), out var etag))
{
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.
// ==========================================================================
using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security;
namespace Squidex.Web;
@ -56,22 +54,4 @@ public static class Extensions
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);
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))
{
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)
{
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 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)

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

@ -37,11 +37,11 @@ public sealed class CachingKeysMiddleware
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"))
{
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)
{
if (!Texts.TryGetValue(culture, out var result))
return Texts.GetOrAdd(culture, culture =>
{
var assembly = typeof(IndexExtensions).Assembly;
@ -74,17 +74,15 @@ public static class IndexExtensions
{
using (var reader = new StreamReader(resourceStream))
{
result = reader.ReadToEnd();
var result = reader.ReadToEnd();
Texts[culture] = result;
return result;
}
}
else
{
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.
// ==========================================================================
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Frontend.Middlewares;
@ -40,12 +41,12 @@ public static class Startup
app.UseMiddleware<NotifoMiddleware>();
app.UseWhen(c => c.IsSpaFile(), builder =>
app.UseWhen(IsSpaFile, builder =>
{
builder.UseMiddleware<SetupMiddleware>();
});
app.UseWhen(c => c.IsSpaFile() || c.IsHtmlPath(), builder =>
app.UseWhen(IsSpaFileOrHtml, builder =>
{
// Adjust the base for all potential html files.
builder.UseHtmlTransform(new HtmlTransformOptions
@ -82,25 +83,42 @@ public static class Startup
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
{
OnPrepareResponse = context =>
{
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
});
}
private static bool IsSpaFileOrHtml(this HttpContext context)
{
return context.IsSpaFile() || context.IsHtmlPath();
}
private static bool IsSpaFile(this HttpContext context)
{
return (context.IsIndex() || !Path.HasExtension(context.Request.Path)) && !context.IsDevServer();

7
backend/src/Squidex/Squidex.csproj

@ -7,7 +7,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>
<NoWarn>1701;1702;NETSDK1206;NU1608</NoWarn>
<NoWarn>1701;1702;CS1591;NETSDK1206;NU1608</NoWarn>
</PropertyGroup>
<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_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_fr.json" Link="Areas\Frontend\Resources\frontend_fr.json" />
</ItemGroup>
<ItemGroup>
@ -156,8 +157,4 @@
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605;IDE0060</NoWarn>
</PropertyGroup>
</Project>

4
backend/src/Squidex/Startup.cs

@ -96,13 +96,13 @@ public sealed class Startup
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");
});
}
app.UseWhen(c => c.Request.Path.StartsWithSegments(Constants.PrefixApi, StringComparison.OrdinalIgnoreCase), builder =>
app.UseWhenPath(Constants.PrefixApi, builder =>
{
builder.UseSquidexCacheKeys();
builder.UseSquidexExceptionHandling();

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

@ -56,7 +56,7 @@ public class ConfigPlansProviderTests
[Theory]
[InlineData(null)]
[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>());

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

@ -27,9 +27,9 @@ public class FileExtensionsTests
[InlineData("")]
[InlineData(" ")]
[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);
}

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

@ -107,6 +107,34 @@ public class HashExtensionsTests
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 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("invalid")]
[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);
}

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

@ -16,7 +16,7 @@ public class StringExtensionsTests
[InlineData("me@@web.com", false)]
[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());
}
@ -25,7 +25,7 @@ public class StringExtensionsTests
[InlineData(null)]
[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"));
}

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

@ -51,7 +51,7 @@ public class ETagCommandMiddlewareTests
[Fact]
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);
@ -61,7 +61,7 @@ public class ETagCommandMiddlewareTests
[Fact]
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);

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

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

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

@ -170,10 +170,10 @@ public class CachingKeysMiddlewareTests
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]
@ -181,10 +181,10 @@ public class CachingKeysMiddlewareTests
{
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]
@ -192,10 +192,10 @@ public class CachingKeysMiddlewareTests
{
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]
@ -358,7 +358,7 @@ public class CachingKeysMiddlewareTests
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);
}
@ -379,7 +379,7 @@ public class CachingKeysMiddlewareTests
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);
}

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">
<ng-container>
<div class="section mb-4">
<div class="section mb-2">
<label for="id">{{ 'common.id' | sqxTranslate }}</label>
<div class="input-group">
@ -11,6 +11,9 @@
</button>
</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">
<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">
<ng-container title>
<div class="d-flex align-items-center">
<a class="btn btn-text-secondary" (click)="back()" *ngIf="schema.type !== 'Singleton'">
<span class="hidden">{{ 'common.back' | sqxTranslate }}</span>
<a class="btn btn-text-secondary" aria-labelledby="content-back" (click)="back()" *ngIf="schema.type !== 'Singleton'">
<span id="content-back" hidden>{{ 'common.back' | sqxTranslate }}</span>
<i class="icon-angle-left"></i>
</a>

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

@ -267,6 +267,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnInit {
.subscribe({
next: content => {
this.contentForm.submitCompleted({ noReset: true });
this.contentForm.load(content.data, true);
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 class="cell-actions cell-actions-left" sqxStopClick>
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" #buttonOptions>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<button type="button" class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" #buttonOptions>
<i class="icon-dots"></i>
</button>

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

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

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

@ -12,8 +12,7 @@
</sqx-editable-title>
</div>
<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>
<span class="hidden">{{ 'common.options' | sqxTranslate }}</span>
<button type="button" class="btn btn-text-secondary" attr.aria-label="{{ 'common.options' | sqxTranslate }}" (click)="dropdown.toggle()" #buttonOptions>
<i class="icon-dots"></i>
</button>

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

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

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

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

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

@ -5,7 +5,7 @@
<div class="row g-0">
<div class="col-auto" *ngIf="schemasState.canCreate | async">
<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"
titlePosition="top-start"
shortcut="CTRL + U"

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

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

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

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

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

@ -29,13 +29,13 @@
<ng-template #simple>
<span [class.truncate]="truncate">
<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>
</span>
</ng-container>
<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>
</span>
</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>) {
if (changes.scheduled) {
if (changes.scheduled || changes.status) {
if (this.scheduled) {
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 => {
this.valueChange$.next(value);
this.updateState(value);
this.updateValue(value);
});
this.updateInitialData();
@ -161,6 +159,8 @@ export class EditContentForm extends Form<ExtendedFormGroup, any> {
if (isInitial) {
this.updateInitialData();
}
this.updateValue(this.form.value);
}
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() {
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);
// Should update client name.
Assert.Equal(updateNameRequest.Name, client_2.Name);
Assert.Equal(updateNameRequest.AllowAnonymous, client_2.AllowAnonymous);
Assert.Equal(updateNameRequest.ApiCallsLimit, client_2.ApiCallsLimit);
Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2.ApiTrafficLimit);
Assert.Equal(updateNameRequest.Role, client_2.Role);
Assert.Equal(updateNameRequest.Name, client_2?.Name);
Assert.Equal(updateNameRequest.AllowAnonymous, client_2?.AllowAnonymous);
Assert.Equal(updateNameRequest.ApiCallsLimit, client_2?.ApiCallsLimit);
Assert.Equal(updateNameRequest.ApiTrafficLimit, client_2?.ApiTrafficLimit);
Assert.Equal(updateNameRequest.Role, client_2?.Role);
await Verify(clients_2)
.IgnoreMember<ClientDto>(x => x.Secret);
@ -111,6 +111,6 @@ public sealed class AppClientsTests : IClassFixture<ClientFixture>
var clients = await app.Apps.PostClientAsync(createRequest);
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]
public sealed class AppContributorsTests : IClassFixture<ClientFixture>
{
private readonly string email = $"{Guid.NewGuid()}@squidex.io";
public ClientFixture _ { get; }
public AppContributorsTests(ClientFixture fixture)
@ -78,7 +76,7 @@ public sealed class AppContributorsTests : IClassFixture<ClientFixture>
// STEP 1: Update contributor role.
var updateRequest = new AssignContributorDto
{
ContributorId = email,
ContributorId = contributor.ContributorId,
// Test update of role.
Role = "Owner"
};
@ -96,30 +94,56 @@ public sealed class AppContributorsTests : IClassFixture<ClientFixture>
var (app, _) = await _.PostAppAsync();
// STEP 1: Assign contributor.
var contributor = await InviteAsync(app);
// STEP 1: Assign first contributor.
await InviteAsync(app, "Owner");
// STEP 2: Assign other contributor.
var contributor2 = await InviteAsync(app, "Owner");
// 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
{
ContributorId = email,
// 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 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 language_2_DE = languages_2.Items.Find(x => x.Iso2Code == "de");
Assert.Equal(updateRequest.Fallback, language_2_DE.Fallback);
Assert.Equal(updateRequest.IsOptional, language_2_DE.IsOptional);
Assert.Equal(updateRequest.Fallback, language_2_DE?.Fallback);
Assert.Equal(updateRequest.IsOptional, language_2_DE?.IsOptional);
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_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.
Assert.False(language_4_EN.IsMaster);
Assert.False(language_4_EN?.IsMaster);
// 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.
Assert.Empty(language_4_IT.Fallback);
Assert.Empty(language_4_IT?.Fallback!);
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");
// Fallback language must be removed.
Assert.Empty(language_2_IT.Fallback);
Assert.Equal(new[] { "en", "it" }, languages_2.Items.Select(x => x.Iso2Code).ToArray());
Assert.Empty(language_2_IT?.Fallback!);
Assert.Equal(new[] { "en", "it" }, languages_2?.Items.Select(x => x.Iso2Code).ToArray());
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);
// Should return role with correct name.
Assert.Equal(updateRequest.Permissions, role_2.Permissions);
Assert.Equal(updateRequest.Permissions, role_2?.Permissions);
await Verify(role_2)
.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);
// Should return role with correct number of users and clients.
Assert.Equal(1, role_2.NumClients);
Assert.Equal(0, role_2.NumContributors);
Assert.Equal(1, role_2?.NumClients);
Assert.Equal(0, role_2?.NumContributors);
// 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);
// Should return role with correct number of users and clients.
Assert.Equal(0, role_2.NumClients);
Assert.Equal(1, role_2.NumContributors);
Assert.Equal(0, role_2?.NumClients);
Assert.Equal(1, role_2?.NumContributors);
// 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);
}
private async Task AssignContributor(string role = null)
private async Task AssignContributor(string? role = null)
{
var assignRequest = new AssignContributorDto
{
@ -167,7 +167,7 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
await _.Client.Apps.PostContributorAsync(assignRequest);
}
private async Task AssignClient(string role = null)
private async Task AssignClient(string? role = null)
{
var updateRequest = new UpdateClientDto
{
@ -187,6 +187,6 @@ public sealed class AppRolesTests : IClassFixture<CreatedAppFixture>
var roles = await _.Client.Apps.PostRoleAsync(createRequest);
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 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);
}
[Fact]
public async Task Should_move_asset()
[Theory]
[InlineData(ContentStrategies.Move.Single)]
[InlineData(ContentStrategies.Move.Bulk)]
public async Task Should_move_asset(ContentStrategies.Move strategy)
{
// STEP 1: Create folder.
var folder1 = await CreateFolderAsync(Guid.NewGuid().ToString());
// 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
var moveRequest = new MoveAssetDto
{
ParentId = folder1.Id
};
await _.Client.Assets.MoveAsync(asset_1, folder1, strategy);
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]
@ -157,7 +156,7 @@ public class AssetFoldersTests : IClassFixture<CreatedAppFixture>
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
{

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

@ -30,9 +30,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata.
Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth);
Assert.Equal(600L, (long)asset.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(AssetType.Image, asset.Type);
@ -119,9 +119,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata.
Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth);
Assert.Equal(600L, (long)asset.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(AssetType.Image, asset.Type);
@ -137,9 +137,9 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
// Should parse image metadata and fix orientation.
Assert.True(asset.IsImage);
Assert.Equal(600L, (long)asset.PixelWidth);
Assert.Equal(600L, (long)asset.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(79L, asset.Metadata["imageQuality"]);
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}";
@ -246,7 +246,7 @@ public class AssetFormatTests : IClassFixture<CreatedAppFixture>
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);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
Assert.Equal(stream.Length, downloaded?.Length);
}
await Verify(asset_1);
@ -64,7 +64,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset);
// 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);
// 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.
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))
{
var downloaded = await _.DownloadAsync(asset_2);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
Assert.Equal(stream.Length, downloaded?.Length);
}
await Verify(asset_2);
@ -201,7 +201,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(progress.Asset);
// 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);
// Should dowload with correct size.
Assert.Equal(stream.Length, downloaded.Length);
Assert.Equal(stream.Length, downloaded?.Length);
}
}
}
[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.
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"
};
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.
Assert.Equal(fileNameRequest.FileName, asset_2.FileName);
await Verify(asset_2);
await Verify(asset_2).UseParameters(strategy);
}
[Fact]
public async Task Should_annote_asset_metadata()
[Theory]
[InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_metadata(ContentStrategies.Annotate strategy)
{
// STEP 1: Create asset.
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.
Assert.Equal(metadataRequest.Metadata, asset_2.Metadata);
await Verify(asset_2);
await Verify(asset_2).UseParameters(strategy);
}
[Fact]
public async Task Should_annote_asset_slug()
[Theory]
[InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_slug(ContentStrategies.Annotate strategy)
{
// STEP 1: Create asset.
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"
};
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.
Assert.Equal(slugRequest.Slug, asset_2.Slug);
await Verify(asset_2);
await Verify(asset_2).UseParameters(strategy);
}
[Fact]
public async Task Should_annote_asset_tags()
[Theory]
[InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_annote_asset_tags(ContentStrategies.Annotate strategy)
{
// STEP 1: Create asset.
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.
Assert.Equal(tagsRequest.Tags, asset_2.Tags);
await Verify(asset_2);
}
[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);
await Verify(asset_2).UseParameters(strategy);
}
[Fact]
public async Task Should_protect_asset()
[Theory]
[InlineData(ContentStrategies.Annotate.Single)]
[InlineData(ContentStrategies.Annotate.Bulk)]
public async Task Should_protect_asset(ContentStrategies.Annotate strategy)
{
// STEP 1: Create asset.
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);
// 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
};
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.
@ -457,7 +477,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode);
}
await Verify(asset_2);
await Verify(asset_2).UseParameters(strategy);
}
[Fact]
@ -486,7 +506,7 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
var downloaded = await _.DownloadAsync(asset_1);
// 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.
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_2.Metadata["blurHash"]);
@ -684,16 +704,18 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Should_delete_asset(bool permanent)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_asset(ContentStrategies.Deletion strategy)
{
// STEP 1: Create asset.
var asset = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Delete asset.
await _.Client.Assets.DeleteAssetAsync(asset.Id, permanent: permanent);
await _.Client.Assets.DeleteAsync(asset, strategy);
// Should return 404 when asset deleted.
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.
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);
@ -716,20 +738,24 @@ public class AssetTests : IClassFixture<CreatedAppFixture>
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));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Should_recreate_deleted_asset(bool permanent)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_asset(ContentStrategies.Deletion strategy)
{
// STEP 1: Create asset.
var asset_1 = await _.Client.Assets.UploadFileAsync("Assets/logo-squared.png", "image/png");
// STEP 2: Delete asset.
await _.Client.Assets.DeleteAssetAsync(asset_1.Id, permanent: permanent);
await _.Client.Assets.DeleteAsync(asset_1, strategy);
// STEP 3: Recreate asset.

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

@ -51,7 +51,7 @@ public class BackupTests : IClassFixture<ClientFixture>
// 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
{
@ -96,7 +96,7 @@ public class BackupTests : IClassFixture<ClientFixture>
// 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
{

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

@ -39,7 +39,7 @@ public class CDNTests : IClassFixture<ClientCloudFixture>
var url = _.CDNClient.GenerateImageUrl(id);
await DownloadAsync(url);
await DownloadAsync(url!);
}
[Fact]
@ -59,7 +59,7 @@ public class CDNTests : IClassFixture<ClientCloudFixture>
var url = _.CloudClient.GenerateImageUrl(id);
await DownloadAsync(url);
await DownloadAsync(url!);
}
[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.
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.
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.
var content = await _.Contents.CreateAsync(new TestEntityData
{
Localized = new Dictionary<string, string>
Localized = new Dictionary<string, string?>
{
["de"] = "Hallo",
["en"] = "Hello"
@ -48,7 +48,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
// STEP 1: Create content.
var content = await _.Contents.CreateAsync(new TestEntityData
{
Localized = new Dictionary<string, string>
Localized = new Dictionary<string, string?>
{
["de"] = "Hallo",
["en"] = "Hello",
@ -62,7 +62,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
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]
@ -71,7 +71,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
// STEP 1: Create content.
var content = await _.Contents.CreateAsync(new TestEntityData
{
Localized = new Dictionary<string, string>
Localized = new Dictionary<string, string?>
{
["de"] = "Hallo",
["en"] = "Hello"
@ -98,7 +98,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
["X-Flatten"] = "1"
});
static void AssertValue(string value, string not = null)
static void AssertValue(string? value, string? not = null)
{
Assert.NotNull(value);
Assert.NotEmpty(value);
@ -111,7 +111,7 @@ public class ContentLanguageTests : IClassFixture<ContentFixture>
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}";

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

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

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

@ -432,7 +432,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
[Fact]
public async Task Should_query_json_with_dot()
{
TestEntity content = null;
TestEntity? content = null;
try
{
// 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);
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
{
@ -528,7 +528,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
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);
}
@ -563,7 +563,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
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);
}
@ -695,7 +695,7 @@ public class ContentQueryTests : IClassFixture<ContentQueryFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JObject>(query);
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]

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

@ -42,7 +42,7 @@ public class ContentReferencesTests : IClassFixture<ContentReferencesFixture>
// STEP 3: Query new item.
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.

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

@ -76,7 +76,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
}
[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
{
@ -151,7 +151,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
}
[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.
var scripts = new SchemaScriptsDto
@ -183,7 +183,7 @@ public class ContentScriptingTests : IClassFixture<CreatedAppFixture>
iv = 99
}
}
}
},
],
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.
var content = await _.Contents.CreateAsync(new TestEntityData
{
Localized = new Dictionary<string, string>
Localized = new Dictionary<string, string?>
{
["en"] = null
}
@ -156,7 +156,7 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
// STEP 2: Get the item and ensure that the text is the same.
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);
}
@ -331,13 +331,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Update.Normal)]
[InlineData(Strategies.Update.Upsert)]
[InlineData(Strategies.Update.UpsertBulk)]
[InlineData(Strategies.Update.Bulk)]
[InlineData(Strategies.Update.BulkShared)]
[InlineData(Strategies.Update.BulkWithSchema)]
public async Task Should_update_content(Strategies.Update strategy)
[InlineData(ContentStrategies.Update.Normal)]
[InlineData(ContentStrategies.Update.Upsert)]
[InlineData(ContentStrategies.Update.UpsertBulk)]
[InlineData(ContentStrategies.Update.Bulk)]
[InlineData(ContentStrategies.Update.BulkShared)]
[InlineData(ContentStrategies.Update.BulkWithSchema)]
public async Task Should_update_content(ContentStrategies.Update strategy)
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
@ -361,13 +361,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Update.Normal)]
[InlineData(Strategies.Update.Upsert)]
[InlineData(Strategies.Update.UpsertBulk)]
[InlineData(Strategies.Update.Bulk)]
[InlineData(Strategies.Update.BulkShared)]
[InlineData(Strategies.Update.BulkWithSchema)]
public async Task Should_update_content_to_null(Strategies.Update strategy)
[InlineData(ContentStrategies.Update.Normal)]
[InlineData(ContentStrategies.Update.Upsert)]
[InlineData(ContentStrategies.Update.UpsertBulk)]
[InlineData(ContentStrategies.Update.Bulk)]
[InlineData(ContentStrategies.Update.BulkShared)]
[InlineData(ContentStrategies.Update.BulkWithSchema)]
public async Task Should_update_content_to_null(ContentStrategies.Update strategy)
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
@ -388,13 +388,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)]
public async Task Should_patch_content(Strategies.Patch strategy)
[InlineData(ContentStrategies.Patch.Normal)]
[InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_content(ContentStrategies.Patch strategy)
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
@ -418,13 +418,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)]
public async Task Should_patch_id_data_value(Strategies.Patch strategy)
[InlineData(ContentStrategies.Patch.Normal)]
[InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_id_data_value(ContentStrategies.Patch strategy)
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
@ -445,13 +445,13 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Patch.Normal)]
[InlineData(Strategies.Patch.Upsert)]
[InlineData(Strategies.Patch.UpsertBulk)]
[InlineData(Strategies.Patch.Bulk)]
[InlineData(Strategies.Patch.BulkShared)]
[InlineData(Strategies.Patch.BulkWithSchema)]
public async Task Should_patch_content_to_null(Strategies.Patch strategy)
[InlineData(ContentStrategies.Patch.Normal)]
[InlineData(ContentStrategies.Patch.Upsert)]
[InlineData(ContentStrategies.Patch.UpsertBulk)]
[InlineData(ContentStrategies.Patch.Bulk)]
[InlineData(ContentStrategies.Patch.BulkShared)]
[InlineData(ContentStrategies.Patch.BulkWithSchema)]
public async Task Should_patch_content_to_null(ContentStrategies.Patch strategy)
{
// STEP 1: Create a new item.
var content = await _.Contents.CreateAsync(new TestEntityData
@ -463,7 +463,7 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
// STEP 2: Patch with selected strategy.
await _.Client.PatchAsync(content, new
{
@string = new { iv = (string)null }
@string = new { iv = (string?)null }
}, strategy);
var updated = await _.Contents.GetAsync(content.Id);
@ -472,11 +472,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)]
public async Task Should_delete_content(Strategies.Deletion strategy)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_content(ContentStrategies.Deletion strategy)
{
// STEP 1: Create a new item.
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));
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]
[InlineData(Strategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)]
public async Task Should_create_content_with_custom_id_and_delete_it(Strategies.Deletion strategy)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_create_content_with_custom_id_and_delete_it(ContentStrategies.Deletion strategy)
{
var id = $"custom-{Guid.NewGuid()}";
@ -534,11 +534,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content(Strategies.Deletion strategy)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content(ContentStrategies.Deletion strategy)
{
// STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData
@ -571,11 +571,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content_with_upsert(Strategies.Deletion strategy)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_recreate_deleted_content_with_upsert(ContentStrategies.Deletion strategy)
{
// STEP 1: Create a new item.
var content_1 = await _.Contents.CreateAsync(new TestEntityData
@ -598,11 +598,11 @@ public class ContentUpdateTests : IClassFixture<ContentFixture>
}
[Theory]
[InlineData(Strategies.Deletion.SingleSoft)]
[InlineData(Strategies.Deletion.SinglePermanent)]
[InlineData(Strategies.Deletion.BulkSoft)]
[InlineData(Strategies.Deletion.BulkPermanent)]
public async Task Should_delete_recreated_content(Strategies.Deletion strategy)
[InlineData(ContentStrategies.Deletion.SingleSoft)]
[InlineData(ContentStrategies.Deletion.SinglePermanent)]
[InlineData(ContentStrategies.Deletion.BulkSoft)]
[InlineData(ContentStrategies.Deletion.BulkPermanent)]
public async Task Should_delete_recreated_content(ContentStrategies.Deletion strategy)
{
var id = $"custom-{Guid.NewGuid()}";

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

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

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

@ -130,7 +130,7 @@ public class GraphQLSubscriptionTests : IClassFixture<ContentFixture>
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());

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);
Assert.Equal(1, result["findMyWritesContent"]["flatData"]["json"]["value"].Value<int>());
Assert.Equal(2, result["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value<int>());
Assert.Equal(1, result?["findMyWritesContent"]?["flatData"]?["json"]?["value"]?.Value<int>());
Assert.Equal(2, result?["findMyWritesContent"]?["flatData"]?["json"]?["obj"]?["value"]?.Value<int>());
}
[Fact]
@ -89,7 +89,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
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>
{
@ -168,7 +168,7 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames =
result["countries"].ToObject<List<Country>>()[0].Data.States
result!["countries"]!.ToObject<List<Country>>()![0].Data.States
.SelectMany(x => x.Data.Cities)
.Select(x => x.Data.Name)
.Order();
@ -204,10 +204,10 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames =
result["countries"]
.SelectMany(x => x["data"]["states"])
.SelectMany(x => x["cities"])
.Select(x => x["data"]["name"].Value<string>())
result!["countries"]!
.SelectMany(x => x["data"]!["states"]!)
.SelectMany(x => x["cities"]!)
.Select(x => x["data"]!["name"]!.Value<string>())
.Order();
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 cityNames =
result["countries"]
.SelectMany(x => x["data"]["states"])
.SelectMany(x => x["cities"])
.Select(x => x["data"]["name"].Value<string>())
result["countries"]!
.SelectMany(x => x["data"]!["states"]!)
.SelectMany(x => x["cities"]!)
.Select(x => x["data"]!["name"]!.Value<string>())
.Order();
Assert.Equal(new[] { "Leipzig" }, cityNames);
@ -270,9 +270,9 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var stateNames =
result["cities"]
.SelectMany(x => x["states"])
.Select(x => x["data"]["name"].Value<string>())
result["cities"]!
.SelectMany(x => x["states"]!)
.Select(x => x["data"]!["name"]!.Value<string>())
.Order();
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 stateNames =
result["cities"]
.SelectMany(x => x["states"])
.Select(x => x["data"]["name"].Value<string>())
result!["cities"]!
.SelectMany(x => x!["states"]!)
.Select(x => x!["data"]!["name"]!.Value<string>())
.Order();
Assert.Equal(new[] { "Saxony" }, stateNames);
@ -322,8 +322,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var cityNames =
result["cities"]
.Select(x => x["data__dynamic"]["name"]["iv"].Value<string>())
result["cities"]!
.Select(x => x["data__dynamic"]!["name"]!["iv"]!.Value<string>())
.Order();
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.
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]
@ -387,8 +387,8 @@ public sealed class GraphQLTests : IClassFixture<GraphQLFixture>
var result = await _.Client.SharedDynamicContents.GraphQlAsync<JToken>(query);
var names =
result["queryContentsByIds"]
.Select(x => x["data"]["name"].Value<string>())
result["queryContentsByIds"]!
.Select(x => x["data"]!["name"]!.Value<string>())
.Order();
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 city1Id = result["a"]["id"].ToString();
var city2Id = result["b"]["id"].ToString();
var city1Id = result!["a"]!["id"]!.ToString();
var city2Id = result!["b"]!["id"]!.ToString();
Assert.Equal(allCities.Items[0].Id, city1Id);
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
{
References = new[]
{
References =
[
referencedContent.Id
}
],
});
@ -138,14 +138,14 @@ public class RuleRunnerTests : IClassFixture<ClientFixture>, IClassFixture<Webho
new SchemaCondition
{
SchemaId = parentSchema.Id
}
},
],
ReferencedSchemas =
[
new SchemaCondition
{
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.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.
// ==========================================================================
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;
@ -26,12 +26,7 @@ public class TeamCreationTests : IClassFixture<ClientFixture>
[Fact]
public async Task Should_create_team()
{
var request = new CreateTeamDto
{
Name = teamName
};
var team = await _.Client.Teams.PostTeamAsync(request);
var team = await _.PostTeamAsync(teamName);
Assert.Equal(teamName, team.Name);
}
@ -39,16 +34,24 @@ public class TeamCreationTests : IClassFixture<ClientFixture>
[Fact]
public async Task Should_create_team_with_duplicate_name()
{
var request = new CreateTeamDto
{
Name = teamName
};
var team1 = await _.Client.Teams.PostTeamAsync(request);
var team2 = await _.Client.Teams.PostTeamAsync(request);
var team1 = await _.PostTeamAsync(teamName);
var team2 = await _.PostTeamAsync(teamName);
Assert.Equal(teamName, team1.Name);
Assert.Equal(teamName, team2.Name);
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>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<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,
Links: {
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 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();

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

Loading…
Cancel
Save