diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index e5114407d..8db73c260 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPatterns Add(Guid id, string name, string pattern, string? message) + public AppPatterns Add(Guid id, string name, string pattern, string? message = null) { var newPattern = new AppPattern(name, pattern, message); @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPatterns Update(Guid id, string name, string pattern, string? message) + public AppPatterns Update(Guid id, string name, string pattern, string? message = null) { Guard.NotNullOrEmpty(name); Guard.NotNullOrEmpty(pattern); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index 1a80e62f0..94b191d58 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -58,5 +58,17 @@ namespace Squidex.Domain.Apps.Core.Apps this.language = language; this.languageFallbacks = fallback ?? Array.Empty(); } + + public LanguageConfig WithoutFallback(Language fallback) + { + Guard.NotNull(fallback); + + if (languageFallbacks.Contains(fallback)) + { + return new LanguageConfig(Language, IsOptional, LanguageFallbacks.Except(new[] { fallback })); + } + + return this; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 88cb22428..0a65412c2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -89,19 +89,26 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public LanguagesConfig MakeMaster(Language language) + public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable? fallback = null) { Guard.NotNull(language); - return Create(languages, languages[language]); + return Set(new LanguageConfig(language, isOptional, fallback)); } [Pure] - public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable? fallback = null) + public LanguagesConfig MakeMaster(Language language) { Guard.NotNull(language); - return Set(new LanguageConfig(language, isOptional, fallback)); + var newMaster = new LanguageConfig(languages[language].Language); + + var newLanguages = + languages + .Without(language) + .With(language, newMaster); + + return Create(newLanguages, newMaster); } [Pure] @@ -111,7 +118,10 @@ namespace Squidex.Domain.Apps.Core.Apps var newLanguages = languages.With(config.Language, config); - var newMaster = Master?.Language == config.Language ? config : Master; + var newMaster = + Master.Language != config.Language ? + Master : + config; return Create(newLanguages, newMaster!); } @@ -122,12 +132,7 @@ namespace Squidex.Domain.Apps.Core.Apps Guard.NotNull(language); var newLanguages = - languages.Values.Where(x => x.Language != language) - .Select(config => new LanguageConfig( - config.Language, - config.IsOptional, - config.LanguageFallbacks.Except(new[] { language }))) - .ToArrayDictionary(x => x.Language); + languages.Values.Where(x => x.Language != language).Select(x => x.WithoutFallback(language)).ToArrayDictionary(x => x.Language); var newMaster = newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ?? diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index a9eeee3a0..a561f0c0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Backup; @@ -155,16 +156,22 @@ namespace Squidex.Domain.Apps.Entities.Apps private async Task ReadAssetAsync(Guid appId, IBackupReader reader) { - await reader.ReadBlobAsync(AvatarFile, async stream => + try { - try - { - await appImageStore.UploadAsync(appId, stream); - } - catch (AssetAlreadyExistsException) + await reader.ReadBlobAsync(AvatarFile, async stream => { - } - }); + try + { + await appImageStore.UploadAsync(appId, stream); + } + catch (AssetAlreadyExistsException) + { + } + }); + } + catch (FileNotFoundException) + { + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index b99e705bc..4a268db7b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -75,15 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - await EnrichWithMetadataAsync(createAsset, createAsset.Tags); - - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - context.Complete(new AssetCreatedResult(asset, false)); - - await assetFileStore.CopyAsync(tempFile, createAsset.AssetId, asset.FileVersion); + await UploadAsync(context, tempFile, createAsset, true, next); } finally { @@ -95,16 +87,11 @@ namespace Squidex.Domain.Apps.Entities.Assets case UpdateAsset updateAsset: { - await EnrichWithMetadataAsync(updateAsset); await EnrichWithHashAndUploadAsync(updateAsset, tempFile); try { - await HandleCoreAsync(context, next); - - var asset = context.Result(); - - await assetFileStore.CopyAsync(tempFile, updateAsset.AssetId, asset.FileVersion); + await UploadAsync(context, tempFile, updateAsset, false, next); } finally { @@ -115,12 +102,24 @@ namespace Squidex.Domain.Apps.Entities.Assets } default: - await HandleCoreAsync(context, next); + await HandleCoreAsync(context, false, next); break; } } - private async Task HandleCoreAsync(CommandContext context, NextDelegate next) + private async Task UploadAsync(CommandContext context, string tempFile, UploadAssetCommand command, bool created, NextDelegate next) + { + await EnrichWithMetadataAsync(command); + + var asset = await HandleCoreAsync(context, created, next); + + if (asset != null) + { + await assetFileStore.CopyAsync(tempFile, command.AssetId, asset.FileVersion); + } + } + + private async Task HandleCoreAsync(CommandContext context, bool created, NextDelegate next) { await base.HandleAsync(context, next); @@ -128,8 +127,19 @@ namespace Squidex.Domain.Apps.Entities.Assets { var enriched = await assetEnricher.EnrichAsync(asset, contextProvider.Context); - context.Complete(enriched); + if (created) + { + context.Complete(new AssetCreatedResult(enriched, false)); + } + else + { + context.Complete(enriched); + } + + return enriched; } + + return null; } private static bool IsDuplicate(IAssetEntity asset, AssetFile file) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs index fc1cbe6e1..39f950630 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeTagGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class FileTypeTagGenerator : IAssetMetadataSource + public sealed class FileTypeAssetMetadataSource : IAssetMetadataSource { public Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs similarity index 50% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index cf929ddc6..52541e19a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -14,11 +14,11 @@ using Squidex.Infrastructure.Assets; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class ImageMetadataSource : IAssetMetadataSource + public sealed class ImageAssetMetadataSource : IAssetMetadataSource { private readonly IAssetThumbnailGenerator assetThumbnailGenerator; - public ImageMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator) + public ImageAssetMetadataSource(IAssetThumbnailGenerator assetThumbnailGenerator) { Guard.NotNull(assetThumbnailGenerator); @@ -27,32 +27,35 @@ namespace Squidex.Domain.Apps.Entities.Assets public async Task EnhanceAsync(UploadAssetCommand command, HashSet? tags) { - var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - - if (imageInfo != null) + if (command.Type == AssetType.Unknown) { - command.Type = AssetType.Image; - - command.Metadata.SetPixelWidth(imageInfo.PixelWidth); - command.Metadata.SetPixelHeight(imageInfo.PixelHeight); + var imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead()); - if (tags != null) + if (imageInfo != null) { - tags.Add("image"); + command.Type = AssetType.Image; - var wh = imageInfo.PixelWidth + imageInfo.PixelHeight; + command.Metadata.SetPixelWidth(imageInfo.PixelWidth); + command.Metadata.SetPixelHeight(imageInfo.PixelHeight); - if (wh > 2000) - { - tags.Add("image/large"); - } - else if (wh > 1000) - { - tags.Add("image/medium"); - } - else + if (tags != null) { - tags.Add("image/small"); + tags.Add("image"); + + var wh = imageInfo.PixelWidth + imageInfo.PixelHeight; + + if (wh > 2000) + { + tags.Add("image/large"); + } + else if (wh > 1000) + { + tags.Add("image/medium"); + } + else + { + tags.Add("image/small"); + } } } } @@ -60,10 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public IEnumerable Format(IAssetEntity asset) { - if (asset.Type == AssetType.Image) - { - yield return $"{asset.Metadata.GetPixelWidth()}x{asset.Metadata.GetPixelHeight()}px"; - } + yield break; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index 052c976b2..ea42baed8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NodaTime; @@ -202,13 +203,17 @@ namespace Squidex.Domain.Apps.Entities.Backup } catch (Exception ex) { - if (ex is BackupRestoreException backupException) + switch (ex) { - Log(backupException.Message); - } - else - { - Log("Failed with internal error"); + case BackupRestoreException backupException: + Log(backupException.Message); + break; + case FileNotFoundException fileNotFoundException: + Log(fileNotFoundException.Message); + break; + default: + Log("Failed with internal error"); + break; } await CleanupAsync(handlers); diff --git a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs index 2672c80de..c62540506 100644 --- a/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs +++ b/backend/src/Squidex.Infrastructure/Collections/ArrayDictionary{TKey,TValue}.cs @@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.Collections var afterIndex = items.Length - index - 1; Array.Copy(items, 0, result, 0, index); - Array.Copy(items, index, result, index, afterIndex); + Array.Copy(items, index + 1, result, index, afterIndex); } return Create(result); diff --git a/backend/src/Squidex.Web/FileExtensions.cs b/backend/src/Squidex.Web/FileExtensions.cs index 3ef88e643..bcd2ad6dc 100644 --- a/backend/src/Squidex.Web/FileExtensions.cs +++ b/backend/src/Squidex.Web/FileExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Http; using Squidex.Infrastructure.Assets; @@ -14,6 +15,16 @@ namespace Squidex.Web { public static AssetFile ToAssetFile(this IFormFile formFile) { + if (string.IsNullOrWhiteSpace(formFile.ContentType)) + { + throw new ValidationException("File content-type is not defined."); + } + + if (string.IsNullOrWhiteSpace(formFile.FileName)) + { + throw new ValidationException("File name is not defined."); + } + return new AssetFile(formFile.FileName, formFile.ContentType, formFile.Length, formFile.OpenReadStream); } } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs index cd9d1d0c5..7e5beb08b 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs @@ -12,8 +12,8 @@ using NJsonSchema; using NSwag; using NSwag.Generation.Processors; using NSwag.Generation.Processors.Contexts; -using Squidex.ClientLibrary.Management; using Squidex.Pipeline.OpenApi; +using Squidex.Web; namespace Squidex.Areas.Api.Config.OpenApi { diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs index 24a31aa72..16ffd1f19 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataExtensions.cs @@ -20,8 +20,8 @@ namespace Squidex.Areas.Api.Config.OpenApi operation.AddQuery("$search", JsonObjectType.String, "Optional OData full text search."); } - operation.AddQuery("$top", JsonObjectType.Number, $"Optional number of {entity} to take."); - operation.AddQuery("$skip", JsonObjectType.Number, $"Optional number of {entity} to skip."); + operation.AddQuery("$top", JsonObjectType.Integer, $"Optional number of {entity} to take."); + operation.AddQuery("$skip", JsonObjectType.Integer, $"Optional number of {entity} to skip."); operation.AddQuery("$orderby", JsonObjectType.String, "Optional OData order definition."); operation.AddQuery("$filter", JsonObjectType.String, "Optional OData filter definition."); } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs index 01329cbdd..5f4bf73d7 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ODataQueryParamsProcessor.cs @@ -26,7 +26,7 @@ namespace Squidex.Areas.Api.Config.OpenApi public bool Process(OperationProcessorContext context) { - if (context.OperationDescription.Path == supportedPath) + if (context.OperationDescription.Path == supportedPath && context.OperationDescription.Method == "get") { var operation = context.OperationDescription.Operation; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index fff15b861..e748503d2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -14,7 +13,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; -using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; @@ -176,10 +174,10 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpPost] [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] + [ProducesResponseType(typeof(AppDto), 200)] [ApiPermission(Permissions.AppUpdateImage)] [ApiCosts(0)] - public async Task UploadImage(string app, [OpenApiIgnore] List file) + public async Task UploadImage(string app, IFormFile file) { var response = await InvokeCommandAsync(CreateCommand(file)); @@ -264,7 +262,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/image")] - [ProducesResponseType(typeof(AppDto), 201)] + [ProducesResponseType(typeof(AppDto), 200)] [ApiPermission(Permissions.AppUpdate)] [ApiCosts(0)] public async Task DeleteImage(string app) @@ -306,16 +304,16 @@ namespace Squidex.Areas.Api.Controllers.Apps return response; } - private static UploadAppImage CreateCommand(IReadOnlyList file) + private UploadAppImage CreateCommand(IFormFile? file) { - if (file.Count != 1) + if (file == null || Request.Form.Files.Count != 1) { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); + var error = new ValidationError($"Can only upload one file, found {Request.Form.Files.Count} files."); - throw new ValidationException("Cannot create asset.", error); + throw new ValidationException("Cannot upload image.", error); } - return new UploadAppImage { File = file[0].ToAssetFile() }; + return new UploadAppImage { File = file.ToAssetFile() }; } private static FileStream GetTempStream() diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 86f3f8874..ee695cd7a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; -using NSwag.Annotations; using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Contents; using Squidex.Domain.Apps.Core.Tags; @@ -20,7 +19,6 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; -using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Validation; @@ -185,7 +183,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [AssetRequestSizeLimit] [ApiPermission(Permissions.AppAssetsCreate)] [ApiCosts(1)] - public async Task PostAsset(string app, [FromQuery] Guid parentId, [OpenApiIgnore] List file) + public async Task PostAsset(string app, [FromQuery] Guid parentId, IFormFile file) { var assetFile = await CheckAssetFileAsync(file); @@ -215,7 +213,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [ProducesResponseType(typeof(AssetDto), 200)] [ApiPermission(Permissions.AppAssetsUpload)] [ApiCosts(1)] - public async Task PutAssetContent(string app, Guid id, [OpenApiIgnore] List file) + public async Task PutAssetContent(string app, Guid id, IFormFile file) { var assetFile = await CheckAssetFileAsync(file); @@ -311,20 +309,11 @@ namespace Squidex.Areas.Api.Controllers.Assets } } - private async Task CheckAssetFileAsync(IReadOnlyList file) + private async Task CheckAssetFileAsync(IFormFile? file) { - if (file.Count != 1) + if (file == null || Request.Form.Files.Count != 1) { - var error = new ValidationError($"Can only upload one file, found {file.Count} files."); - - throw new ValidationException("Cannot create asset.", error); - } - - var formFile = file[0]; - - if (formFile.Length > assetOptions.MaxSize) - { - var error = new ValidationError($"File cannot be bigger than {assetOptions.MaxSize.ToReadableSize()}."); + var error = new ValidationError($"Can only upload one file, found {Request.Form.Files.Count} files."); throw new ValidationException("Cannot create asset.", error); } @@ -333,14 +322,14 @@ namespace Squidex.Areas.Api.Controllers.Assets var currentSize = await assetStatsRepository.GetTotalSizeAsync(AppId); - if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + formFile.Length) + if (plan.MaxAssetSize > 0 && plan.MaxAssetSize < currentSize + file.Length) { var error = new ValidationError("You have reached your max asset size."); throw new ValidationException("Cannot create asset.", error); } - return formFile.ToAssetFile(); + return file.ToAssetFile(); } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 626039357..31dc132d7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -160,10 +160,12 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models public static AssetDto FromAsset(IEnrichedAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) { - var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); + var response = SimpleMapper.Map(asset, new AssetDto()); response.Tags = asset.TagNames; + response.FileType = asset.FileName.FileType(); + if (isDuplicate) { response.Meta = new AssetMeta diff --git a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs index 62726a9bb..5c9a74e67 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Languages/LanguagesController.cs @@ -36,7 +36,7 @@ namespace Squidex.Areas.Api.Controllers.Languages /// [HttpGet] [Route("languages/")] - [ProducesResponseType(typeof(string[]), 200)] + [ProducesResponseType(typeof(LanguageDto[]), 200)] [ApiPermission] public IActionResult GetLanguages() { diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index c70d7616a..3765dc695 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -52,11 +52,14 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); - services.AddSingletonAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .As(); } public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 20e44531c..6b88fd368 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -93,9 +93,11 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_revoke_client() { - var clients_1 = clients_0.Revoke("1"); + var clients_1 = clients_0.Add("2", "secret2"); + var clients_2 = clients_1.Add("3", "secret3"); + var clients_3 = clients_2.Revoke("2"); - Assert.Empty(clients_1); + Assert.Equal(new string[] { "1", "3" }, clients_3.Keys); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs index 495ae9c21..21f7bdba0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs @@ -48,9 +48,11 @@ namespace Squidex.Domain.Apps.Core.Model.Apps public void Should_remove_contributor() { var contributors_1 = contributors_0.Assign("1", Role.Developer); - var contributors_2 = contributors_1.Remove("1"); + var contributors_2 = contributors_1.Assign("2", Role.Developer); + var contributors_3 = contributors_2.Assign("3", Role.Developer); + var contributors_4 = contributors_3.Remove("2"); - Assert.Empty(contributors_2); + Assert.Equal(new string[] { "1", "3" }, contributors_4.Keys); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs index 557d216ae..71e1391fb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPatternsTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; using Xunit; @@ -68,9 +69,10 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_remove_pattern() { - var patterns_1 = patterns_0.Remove(firstId); + var patterns_1 = patterns_0.Add(id, "Name1", "Pattern1"); + var patterns_2 = patterns_1.Remove(firstId); - Assert.Empty(patterns_1); + Assert.Equal(id, patterns_2.Keys.Single()); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index b66614b54..abab0038c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -14,15 +14,17 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure; using Xunit; +#pragma warning disable SA1310 // Field names must not contain underscore + namespace Squidex.Domain.Apps.Core.Model.Apps { public class LanguagesConfigTests { + private readonly LanguagesConfig config_0 = LanguagesConfig.Build(Language.DE); + [Fact] public void Should_create_initial_config_0() { - var config_0 = LanguagesConfig.Build(Language.DE); - config_0.OfType().ToList().Should().BeEquivalentTo( new List { @@ -37,9 +39,9 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_create_initial_config_0_with_multiple_languages() { - var config_0 = LanguagesConfig.Build(Language.DE, Language.EN, Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.EN, Language.IT); - config_0.OfType().ToList().Should().BeEquivalentTo( + config.OfType().ToList().Should().BeEquivalentTo( new List { new LanguageConfig(Language.DE), @@ -47,12 +49,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps new LanguageConfig(Language.IT) }); - config_0.TryGetConfig(Language.DE, out var master); + config.TryGetConfig(Language.DE, out var master); - Assert.Equal(Language.DE, config_0.Master.Language); - Assert.Equal(3, config_0.Count); + Assert.Equal(Language.DE, config.Master.Language); + Assert.Equal(3, config.Count); - Assert.Same(master, config_0.Master); + Assert.Same(master, config.Master); } [Fact] @@ -64,18 +66,17 @@ namespace Squidex.Domain.Apps.Core.Model.Apps new LanguageConfig(Language.EN), new LanguageConfig(Language.IT) }; - var config_0 = LanguagesConfig.Build(configs); + var config = LanguagesConfig.Build(configs); - config_0.OfType().ToList().Should().BeEquivalentTo(configs); + config.OfType().ToList().Should().BeEquivalentTo(configs); - Assert.Equal(configs[0], config_0.Master); - Assert.Same(configs[0], config_0.Master); + Assert.Equal(configs[0], config.Master); + Assert.Same(configs[0], config.Master); } [Fact] public void Should_add_language() { - var config_0 = LanguagesConfig.Build(Language.DE); var config_1 = config_0.Set(Language.IT); config_1.OfType().ToList().Should().BeEquivalentTo( @@ -92,36 +93,30 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_make_first_language_to_master() { - var config_0 = LanguagesConfig.Build(Language.IT); - - Assert.Equal(Language.IT, config_0.Master.Language); + Assert.Equal(Language.DE, config_0.Master.Language); } [Fact] public void Should_not_throw_exception_if_language_to_add_already_exists() { - var config_0 = LanguagesConfig.Build(Language.DE); - config_0.Set(Language.DE); } [Fact] public void Should_make_master_language() { - var config_0 = LanguagesConfig.Build(Language.DE); - var config_1 = config_0.Set(Language.UK); - var config_2 = config_1.Set(Language.IT); + var config_2 = config_1.Set(Language.IT, true, new[] { Language.UK }); var config_3 = config_2.MakeMaster(Language.IT); Assert.Equal(Language.IT, config_3.Master.Language); + + Assert.False(config_3.Master.IsOptional); } [Fact] public void Should_return_same_languages_if_master_language_is_already_master() { - var config_0 = LanguagesConfig.Build(Language.DE); - var config_1 = config_0.Set(Language.UK); var config_2 = config_1.Set(Language.IT); var config_3 = config_2.MakeMaster(Language.IT); @@ -133,24 +128,20 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_throw_exception_if_language_to_make_master_is_not_found() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config_0.MakeMaster(Language.EN)); } [Fact] public void Should_not_throw_exception_if_language_is_already_master_language() { - var config_0 = LanguagesConfig.Build(Language.DE); - config_0.MakeMaster(Language.DE); } [Fact] public void Should_remove_language() { - var config_0 = LanguagesConfig.Build(Language.DE, Language.IT, Language.RU); - var config_1 = config_0.Remove(Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.IT, Language.RU); + var config_1 = config.Remove(Language.IT); Assert.Same(config_1.Master, config_1.OfType().FirstOrDefault(x => x.Language == Language.DE)); @@ -165,12 +156,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_remove_fallbacks_when_removing_language() { - var config_0 = + var config = LanguagesConfig.Build( new LanguageConfig(Language.DE), new LanguageConfig(Language.IT, false, Language.RU, Language.IT), new LanguageConfig(Language.RU, false, Language.DE, Language.IT)); - var config_1 = config_0.Remove(Language.IT); + var config_1 = config.Remove(Language.IT); config_1.OfType().ToList().Should().BeEquivalentTo( new List @@ -181,9 +172,8 @@ namespace Squidex.Domain.Apps.Core.Model.Apps } [Fact] - public void Should_do_nothing_if_language_to_remove_is_not_found() + public void Should_return_same_languages_if_language_to_remove_is_not_found() { - var config_0 = LanguagesConfig.Build(Language.DE); var config_1 = config_0.Remove(Language.EN); Assert.Equal(config_0, config_1); @@ -192,15 +182,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_throw_exception_if_language_to_remove_is_master() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config_0.Remove(Language.DE)); } [Fact] public void Should_update_language() { - var config_0 = LanguagesConfig.Build(Language.DE); var config_1 = config_0.Set(new LanguageConfig(Language.IT)); var config_2 = config_1.Set(new LanguageConfig(Language.IT, true, Language.DE)); @@ -215,16 +202,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_throw_exception_if_fallback_language_is_invalid() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config_0.Set(new LanguageConfig(Language.DE, false, Language.EN))); } [Fact] public void Should_provide_enumerators() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.NotEmpty(config_0); Assert.NotNull(((IEnumerable)config_0).GetEnumerator()); @@ -234,8 +217,6 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_resolve_language_by_string() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.True(config_0.TryGetItem("de", out var item)); Assert.NotNull(item); } @@ -243,8 +224,6 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_not_resolve_language_fór_invalid_language() { - var config_0 = LanguagesConfig.Build(Language.DE); - Assert.False(config_0.TryGetItem("invalid", out var item)); Assert.Null(item); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index f0b144b15..38a6674e0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -86,9 +86,11 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_remove_role() { - var roles_1 = roles_0.Remove(firstRole); + var roles_1 = roles_0.Add("role1"); + var roles_2 = roles_1.Add("role2"); + var roles_3 = roles_2.Remove(firstRole); - Assert.Equal(0, roles_1.CustomCount); + Assert.Equal(new string[] { "role1", "role2" }, roles_3.Custom.Select(x => x.Name)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs similarity index 92% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs index 6b9abd742..ea31b9f99 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeTagGeneratorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs @@ -14,10 +14,10 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class FileTypeTagGeneratorTests + public class FileTypeAssetMetadataSourceTests { private readonly HashSet tags = new HashSet(); - private readonly FileTypeTagGenerator sut = new FileTypeTagGenerator(); + private readonly FileTypeAssetMetadataSource sut = new FileTypeAssetMetadataSource(); [Fact] public async Task Should_not_add_tag_if_no_file_info() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs similarity index 79% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 61eea86a9..5fed7180d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Assets; @@ -17,19 +16,30 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class ImageMetadataSourceTests + public class ImageAssetMetadataSourceTests { private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly HashSet tags = new HashSet(); private readonly MemoryStream stream = new MemoryStream(); private readonly AssetFile file; - private readonly ImageMetadataSource sut; + private readonly ImageAssetMetadataSource sut; - public ImageMetadataSourceTests() + public ImageAssetMetadataSourceTests() { file = new AssetFile("MyImage.png", "image/png", 1024, () => stream); - sut = new ImageMetadataSource(assetThumbnailGenerator); + sut = new ImageAssetMetadataSource(assetThumbnailGenerator); + } + + [Fact] + public async Task Should_not_enhance_if_type_already_found() + { + var command = new CreateAsset { File = file, Type = AssetType.Image }; + + await sut.EnhanceAsync(command, tags); + + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A.Ignored)) + .MustNotHaveHappened(); } [Fact] @@ -85,24 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } [Fact] - public void Should_format_with_dimensions_if_image() - { - var source = new AssetEntity - { - Metadata = - new AssetMetadata() - .SetPixelWidth(800) - .SetPixelHeight(600), - Type = AssetType.Image - }; - - var formatted = sut.Format(source).First(); - - Assert.Equal("800x600px", formatted); - } - - [Fact] - public void Should_format_to_empty_if_not_an_image() + public void Should_format_to_empty() { var source = new AssetEntity(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs index 95c702b93..e1c8d9e10 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryJsonConversionTests.cs @@ -361,6 +361,14 @@ namespace Squidex.Infrastructure.Queries AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); } + [Fact] + public void Should_parse_query_with_top() + { + var json = new { skip = 10, top = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + + AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + } + [Fact] public void Should_parse_query_with_sorting() { diff --git a/backend/tools/LoadTest/LoadTest.sln b/backend/tools/LoadTest/LoadTest.sln deleted file mode 100644 index ce256f737..000000000 --- a/backend/tools/LoadTest/LoadTest.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29123.88 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoadTest", "LoadTest.csproj", "{EC732B4F-576E-4853-880D-AA676966D017}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EC732B4F-576E-4853-880D-AA676966D017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC732B4F-576E-4853-880D-AA676966D017}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC732B4F-576E-4853-880D-AA676966D017}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC732B4F-576E-4853-880D-AA676966D017}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {8ABC073D-D6C6-47F0-AFB6-D42F177D468A} - EndGlobalSection -EndGlobal diff --git a/backend/tools/LoadTest/ManyItemsTests.cs b/backend/tools/LoadTest/ManyItemsTests.cs deleted file mode 100644 index ecbe3fa35..000000000 --- a/backend/tools/LoadTest/ManyItemsTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using LoadTest.Model; -using LoadTest.Utils; -using Squidex.ClientLibrary; -using Xunit; - -namespace LoadTest -{ - public class ManyItemsTests - { - private readonly SquidexClient client; - - public ManyItemsTests() - { - client = TestClient.BuildAsync("multiple").Result; - } - - [Fact] - public async Task Should_read_many_async() - { - var contents = await client.GetAsync(); - - for (var i = contents.Total; i < 20000; i++) - { - await client.CreateAsync(new TestEntityData - { - String = RandomString.Create(1000) - }, publish: true); - } - - var found = await client.GetAll2Async(); - - Assert.Equal(20000, found.Items.Count); - } - } - - public static class SquidexClientExtensions - { - public static async Task> GetAll2Async(this SquidexClient client, int batchSize = 200) - where TEntity : SquidexEntityBase - where TData : class, new() - { - var query = new ODataQuery { Top = batchSize, Skip = 0 }; - - var entities = new SquidexEntities(); - do - { - var getResult = await client.GetAsync(query); - - entities.Total = getResult.Total; - entities.Items.AddRange(getResult.Items); - - query.Skip += getResult.Items.Count; - } - while (query.Skip < entities.Total); - - return entities; - } - } -} diff --git a/backend/tools/LoadTest/Model/TestClient.cs b/backend/tools/LoadTest/Model/TestClient.cs deleted file mode 100644 index edbd61294..000000000 --- a/backend/tools/LoadTest/Model/TestClient.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; -using Squidex.ClientLibrary; -using Squidex.ClientLibrary.Management; - -namespace LoadTest.Model -{ - public static class TestClient - { - public const string ServerUrl = "http://localhost:5000"; - - public const string ClientId = "root"; - public const string ClientSecret = "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="; - - public const string TestAppName = "integration-tests"; - - public static readonly SquidexClientManager ClientManager = - new SquidexClientManager( - ServerUrl, - TestAppName, - ClientId, - ClientSecret) - { - ReadResponseAsString = true - }; - - public static async Task> BuildAsync(string schemaName) - { - await CreateAppIfNotExistsAsync(); - await CreateSchemaIfNotExistsAsync(schemaName); - - return ClientManager.GetClient(schemaName); - } - - private static async Task CreateAppIfNotExistsAsync() - { - try - { - var apps = ClientManager.CreateAppsClient(); - - await apps.PostAppAsync(new CreateAppDto - { - Name = TestAppName - }); - } - catch (SquidexManagementException ex) - { - if (ex.StatusCode != 400) - { - throw; - } - } - } - - private static async Task CreateSchemaIfNotExistsAsync(string schemaName) - { - try - { - var schemas = ClientManager.CreateSchemasClient(); - - await schemas.PostSchemaAsync(TestAppName, new CreateSchemaDto - { - Name = schemaName, - Fields = new List - { - new UpsertSchemaFieldDto - { - Name = "number", - Properties = new NumberFieldPropertiesDto - { - IsRequired = true - } - }, - new UpsertSchemaFieldDto - { - Name = "string", - Properties = new StringFieldPropertiesDto - { - IsRequired = false - } - } - }, - IsPublished = true - }); - } - catch (SquidexManagementException ex) - { - if (ex.StatusCode != 400) - { - throw; - } - } - } - } -} \ No newline at end of file diff --git a/backend/tools/LoadTest/ReadingFixture.cs b/backend/tools/LoadTest/ReadingFixture.cs deleted file mode 100644 index dbad0db64..000000000 --- a/backend/tools/LoadTest/ReadingFixture.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using LoadTest.Model; -using Squidex.ClientLibrary; -using Squidex.ClientLibrary.Management; - -namespace LoadTest -{ - public sealed class ReadingFixture : IDisposable - { - public SquidexClient Client { get; private set; } - - public IAppsClient AppsClient { get; private set; } - - public ReadingFixture() - { - Task.Run(async () => - { - Client = await TestClient.BuildAsync("reading"); - - var contents = await Client.GetAllAsync(); - - if (contents.Total != 10) - { - foreach (var content in contents.Items) - { - await Client.DeleteAsync(content); - } - - for (var i = 10; i > 0; i--) - { - await Client.CreateAsync(new TestEntityData { Number = i }, true); - } - } - - AppsClient = TestClient.ClientManager.CreateAppsClient(); - }).Wait(); - } - - public void Dispose() - { - } - } -} diff --git a/backend/tools/LoadTest/TestUtils.cs b/backend/tools/LoadTest/TestUtils.cs deleted file mode 100644 index 441e04339..000000000 --- a/backend/tools/LoadTest/TestUtils.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks; -using LoadTest.Model; -using Squidex.ClientLibrary.Management; -using Xunit; - -namespace LoadTest -{ - public sealed class TestUtils - { - [Fact] - public async Task GenerateAppManyContributorsAsync() - { - var client = TestClient.ClientManager.CreateAppsClient(); - - for (var i = 0; i < 200; i++) - { - await client.PostContributorAsync("test", new AssignContributorDto - { - ContributorId = $"hello{i}@squidex.io", Invite = true, Role = "Editor" - }); - } - } - } -} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs new file mode 100644 index 000000000..b0e87084d --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppCreationTests.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; +using TestSuite.Fixtures; +using Xunit; + +#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 AppCreationTests : IClassFixture + { + public ClientFixture _ { get; } + + public AppCreationTests(ClientFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_create_app() + { + var appName = Guid.NewGuid().ToString(); + + // STEP 1: Create app + var createRequest = new CreateAppDto { Name = appName }; + + var app = await _.Apps.PostAppAsync(createRequest); + + // Should return create app with correct name. + Assert.Equal(appName, app.Name); + + + // STEP 2: Get all apps + var apps = await _.Apps.GetAppsAsync(); + + // Should provide new app when apps are queried. + Assert.Contains(apps, x => x.Name == appName); + + + // STEP 3: Check contributors + var contributors = await _.Apps.GetContributorsAsync(appName); + + // Should not client itself as a contributor. + Assert.Empty(contributors.Items); + + + // STEP 4: Check clients + var clients = await _.Apps.GetClientsAsync(appName); + + // Should create default client. + Assert.Contains(clients.Items, x => x.Id == "default"); + } + + [Fact] + public async Task Should_remove_app() + { + var appName = Guid.NewGuid().ToString(); + + // STEP 1: Create app + var createRequest = new CreateAppDto { Name = appName }; + + await _.Apps.PostAppAsync(createRequest); + + + // STEP 2: Archive app + await _.Apps.DeleteAppAsync(appName); + + var apps = await _.Apps.GetAppsAsync(); + + // Should not provide deleted app when apps are queried. + Assert.DoesNotContain(apps, x => x.Name == appName); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs new file mode 100644 index 000000000..8bce2ac14 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs @@ -0,0 +1,367 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; +using TestSuite.Fixtures; +using Xunit; + +#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 sealed class AppTests : IClassFixture + { + public CreatedAppFixture _ { get; } + + public AppTests(CreatedAppFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_get_app() + { + var app = await _.Apps.GetAppAsync(_.AppName); + + Assert.Equal(_.AppName, app.Name); + } + + [Fact] + public async Task Should_manage_app_properties() + { + var appLabel = Guid.NewGuid().ToString(); + var appDescription = Guid.NewGuid().ToString(); + + // STEP 1: Update app + var updateRequest = new UpdateAppDto { Label = appLabel, Description = appDescription }; + + var app_1 = await _.Apps.UpdateAppAsync(_.AppName, updateRequest); + + Assert.Equal(appLabel, app_1.Label); + Assert.Equal(appDescription, app_1.Description); + } + + [Fact] + public async Task Should_manage_image() + { + // STEP 1: Upload image. + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var file = new FileParameter(stream, "logo-squared.png", "image/png"); + + var app_1 = await _.Apps.UploadImageAsync(_.AppName, file); + + // Should contain image link. + Assert.True(app_1._links.ContainsKey("image")); + } + + + // STEP 2: Download image. + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var temp = new MemoryStream(); + + var downloaded = new MemoryStream(); + + using (var imageStream = await _.Apps.GetImageAsync(_.AppName)) + { + await imageStream.Stream.CopyToAsync(downloaded); + } + + // Should dowload with correct size. + Assert.True(downloaded.Length < stream.Length); + } + + + // STEP 3: Delete Image. + var app_2 = await _.Apps.DeleteImageAsync(_.AppName); + + // Should contain image link. + Assert.False(app_2._links.ContainsKey("image")); + } + + [Fact] + public async Task Should_manage_clients() + { + var clientId = Guid.NewGuid().ToString(); + var clientName = "My Client"; + var clientRole1 = "Editor"; + var clientRole2 = "Owner"; + + // STEP 1: Create client. + var createRequest = new CreateClientDto { Id = clientId }; + + var clients_1 = await _.Apps.PostClientAsync(_.AppName, createRequest); + var client_1 = clients_1.Items.Single(x => x.Id == clientId); + + // Should return client with correct name and id. + Assert.Equal(clientRole1, client_1.Role); + Assert.Equal(clientId, client_1.Name); + + + // STEP 2: Update client name. + var updateNameRequest = new UpdateClientDto { Name = clientName }; + + var clients_2 = await _.Apps.PutClientAsync(_.AppName, clientId, updateNameRequest); + var client_2 = clients_2.Items.Single(x => x.Id == clientId); + + // Should update client name. + Assert.Equal(clientName, client_2.Name); + + + // STEP 3: Update client role. + var updateRoleRequest = new UpdateClientDto { Role = clientRole2 }; + + var clients_3 = await _.Apps.PutClientAsync(_.AppName, clientId, updateRoleRequest); + var client_3 = clients_3.Items.Single(x => x.Id == clientId); + + // Should update client role. + Assert.Equal(clientRole2, client_3.Role); + + + // STEP 4: Delete client + var clients_4 = await _.Apps.DeleteClientAsync(_.AppName, clientId); + + // Should not return deleted client. + Assert.DoesNotContain(clients_4.Items, x => x.Id == clientId); + } + + [Fact] + public async Task Should_manage_contributors() + { + var contributorEmail = "hello@squidex.io"; + var contributorRole1 = "Developer"; + var contributorRole2 = "Owner"; + + // STEP 0: Do not invite contributors when flag is false. + var createRequest = new AssignContributorDto { ContributorId = "test@squidex.io" }; + + var ex = await Assert.ThrowsAsync(() => + { + return _.Apps.PostContributorAsync(_.AppName, createRequest); + }); + + Assert.Equal(404, ex.StatusCode); + + + // STEP 1: Assign contributor. + var createInviteRequest = new AssignContributorDto { ContributorId = contributorEmail, Invite = true }; + + var contributors_1 = await _.Apps.PostContributorAsync(_.AppName, createInviteRequest); + var contributor_1 = contributors_1.Items.FirstOrDefault(x => x.ContributorName == contributorEmail); + + // Should return contributor with correct email. + Assert.Equal(contributorRole1, contributor_1.Role); + + + // STEP 2: Update contributor role. + var updateRequest = new AssignContributorDto { ContributorId = contributorEmail, Role = contributorRole2 }; + + var contributors_2 = await _.Apps.PostContributorAsync(_.AppName, updateRequest); + var contributor_2 = contributors_2.Items.FirstOrDefault(x => x.ContributorId == contributor_1.ContributorId); + + // Should return contributor with correct role. + Assert.Equal(contributorRole2, contributor_2.Role); + + + // STEP 3: Remove contributor. + var contributors3 = await _.Apps.DeleteContributorAsync(_.AppName, contributor_2.ContributorId); + + // Should not return deleted contributor. + Assert.DoesNotContain(contributors3.Items, x => x.ContributorId == contributor_2.ContributorId); + } + + [Fact] + public async Task Should_manage_roles() + { + var roleName = Guid.NewGuid().ToString(); + var roleClient = Guid.NewGuid().ToString(); + var roleContributor1 = "role@squidex.io"; + var roleContributor2 = "role2@squidex.io"; + + // STEP 1: Add role. + var createRequest = new AddRoleDto { Name = roleName }; + + var roles_1 = await _.Apps.PostRoleAsync(_.AppName, createRequest); + var role_1 = roles_1.Items.Single(x => x.Name == roleName); + + // Should return role with correct name. + Assert.Empty(role_1.Permissions); + + + // STEP 2: Update role. + var updateRequest = new UpdateRoleDto { Permissions = new List { "a", "b" } }; + + var roles_2 = await _.Apps.PutRoleAsync(_.AppName, roleName, updateRequest); + var role_2 = roles_2.Items.Single(x => x.Name == roleName); + + // Should return role with correct name. + Assert.Equal(updateRequest.Permissions, role_2.Permissions); + + + // STEP 3: Assign client and contributor. + await _.Apps.PostClientAsync(_.AppName, new CreateClientDto { Id = roleClient }); + + await _.Apps.PutClientAsync(_.AppName, roleClient, new UpdateClientDto { Role = roleName }); + + await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor1, Role = roleName, Invite = true }); + await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor2, Role = roleName, Invite = true }); + + var roles_3 = await _.Apps.GetRolesAsync(_.AppName); + var role_3 = roles_3.Items.Single(x => x.Name == roleName); + + // Should return role with correct number of users and clients. + Assert.Equal(1, role_3.NumClients); + Assert.Equal(2, role_3.NumContributors); + + + // STEP 4: Try to delete role. + var ex = await Assert.ThrowsAsync>(() => + { + return _.Apps.DeleteRoleAsync(_.AppName, roleName); + }); + + Assert.Equal(400, ex.StatusCode); + + + // STEP 5: Remove after client and contributor removed. + var fallbackRole = "Developer"; + + await _.Apps.PutClientAsync(_.AppName, roleClient, new UpdateClientDto { Role = fallbackRole }); + + await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor1, Role = fallbackRole }); + await _.Apps.PostContributorAsync(_.AppName, new AssignContributorDto { ContributorId = roleContributor2, Role = fallbackRole }); + + await _.Apps.DeleteRoleAsync(_.AppName, roleName); + + var roles_4 = await _.Apps.GetRolesAsync(_.AppName); + + // Should not return deleted role. + Assert.DoesNotContain(roles_4.Items, x => x.Name == roleName); + } + + [Fact] + public async Task Should_manage_patterns() + { + var patternName = Guid.NewGuid().ToString(); + var patternRegex1 = Guid.NewGuid().ToString(); + var patternRegex2 = Guid.NewGuid().ToString(); + + // STEP 1: Add pattern. + var createRequest = new UpdatePatternDto { Name = patternName, Pattern = patternRegex1 }; + + var patterns_1 = await _.Apps.PostPatternAsync(_.AppName, createRequest); + var pattern_1 = patterns_1.Items.Single(x => x.Name == patternName); + + // Should return pattern with correct regex. + Assert.Equal(patternRegex1, pattern_1.Pattern); + + + // STEP 2: Update pattern. + var updateRequest = new UpdatePatternDto { Name = patternName, Pattern = patternRegex2 }; + + var patterns_2 = await _.Apps.PutPatternAsync(_.AppName, pattern_1.Id.ToString(), updateRequest); + var pattern_2 = patterns_2.Items.Single(x => x.Name == patternName); + + // Should return pattern with correct regex. + Assert.Equal(patternRegex2, pattern_2.Pattern); + + + // STEP 3: Remove pattern. + var patterns_3 = await _.Apps.DeletePatternAsync(_.AppName, pattern_2.Id.ToString()); + + // Should not return deleted pattern. + Assert.DoesNotContain(patterns_3.Items, x => x.Id == pattern_2.Id); + } + + [Fact] + public async Task Should_manage_languages() + { + var appName = Guid.NewGuid().ToString(); + + // STEP 1: Add app. + var createRequest = new CreateAppDto { Name = appName }; + + await _.Apps.PostAppAsync(createRequest); + + + // STEP 2: Add languages. + await _.Apps.PostLanguageAsync(appName, new AddLanguageDto { Language = "de" }); + await _.Apps.PostLanguageAsync(appName, new AddLanguageDto { Language = "it" }); + await _.Apps.PostLanguageAsync(appName, new AddLanguageDto { Language = "fr" }); + + var languages_1 = await _.Apps.GetLanguagesAsync(appName); + var languageEN_1 = languages_1.Items.First(x => x.Iso2Code == "en"); + + Assert.Equal(new string[] { "en", "de", "fr", "it" }, languages_1.Items.Select(x => x.Iso2Code).ToArray()); + Assert.True(languageEN_1.IsMaster); + + + // STEP 3: Update English language. + var updateRequest1 = new UpdateLanguageDto + { + Fallback = new string[] + { + "fr", + "it" + }, + IsOptional = true + }; + + var languages_2 = await _.Apps.PutLanguageAsync(appName, "de", updateRequest1); + var languageDE_2 = languages_2.Items.First(x => x.Iso2Code == "de"); + + Assert.Equal(new string[] { "fr", "it" }, languageDE_2.Fallback.ToArray()); + Assert.True(languageDE_2.IsOptional); + + + // STEP 4: Update German language. + var updateRequest2 = new UpdateLanguageDto + { + Fallback = new string[] + { + "fr", + "it" + } + }; + + var languages_3 = await _.Apps.PutLanguageAsync(appName, "en", updateRequest2); + var languageEN_3 = languages_3.Items.First(x => x.Iso2Code == "en"); + + Assert.Equal(new string[] { "fr", "it" }, languageEN_3.Fallback.ToArray()); + + + // STEP 5: Change master language. + var masterRequest = new UpdateLanguageDto { IsMaster = true }; + + var languages_4 = await _.Apps.PutLanguageAsync(appName, "de", masterRequest); + + var languageDE_4 = languages_4.Items.First(x => x.Iso2Code == "de"); + var languageEN_4 = languages_4.Items.First(x => x.Iso2Code == "en"); + + Assert.True(languageDE_4.IsMaster); + Assert.False(languageDE_4.IsOptional); + Assert.False(languageEN_4.IsMaster); + Assert.Empty(languageDE_4.Fallback); + Assert.Equal(new string[] { "de", "en", "fr", "it" }, languages_4.Items.Select(x => x.Iso2Code).ToArray()); + + + // STEP 6: Remove language. + var languages_5 = await _.Apps.DeleteLanguageAsync(appName, "fr"); + var languageEN_5 = languages_5.Items.First(x => x.Iso2Code == "en"); + + Assert.Equal(new string[] { "it" }, languageEN_5.Fallback.ToArray()); + Assert.Equal(new string[] { "de", "en", "it" }, languages_5.Items.Select(x => x.Iso2Code).ToArray()); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs new file mode 100644 index 000000000..f81db98c4 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; +using TestSuite.Fixtures; +using Xunit; + +#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 AssetFormatTests : IClassFixture + { + public AssetFixture _ { get; } + + public AssetFormatTests(AssetFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_upload_image_gif() + { + var asset = await _.UploadFileAsync("Assets/SampleGIFImage_40kbmb.gif", "image/gif"); + + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(312, asset.PixelHeight); + Assert.Equal(312, asset.PixelWidth); + Assert.Equal(312L, asset.Metadata["pixelHeight"]); + Assert.Equal(312L, asset.Metadata["pixelWidth"]); + Assert.Equal(AssetType.Image, asset.Type); + } + + [Fact] + public async Task Should_upload_image_gif_without_extension() + { + var asset = await _.UploadFileAsync("Assets/SampleGIFImage_40kbmb.gif", "image/gif", Guid.NewGuid().ToString()); + + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(312, asset.PixelHeight); + Assert.Equal(312, asset.PixelWidth); + Assert.Equal(312L, asset.Metadata["pixelHeight"]); + Assert.Equal(312L, asset.Metadata["pixelWidth"]); + Assert.Equal(AssetType.Image, asset.Type); + } + + [Fact] + public async Task Should_upload_image_png() + { + var asset = await _.UploadFileAsync("Assets/SamplePNGImage_100kbmb.png", "image/png"); + + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(170, asset.PixelHeight); + Assert.Equal(272, asset.PixelWidth); + Assert.Equal(170L, asset.Metadata["pixelHeight"]); + Assert.Equal(272L, asset.Metadata["pixelWidth"]); + Assert.Equal(AssetType.Image, asset.Type); + } + + [Fact] + public async Task Should_upload_image_jpg() + { + var asset = await _.UploadFileAsync("Assets/SampleJPGImage_50kbmb.jpg", "image/jpg"); + + // Should parse image metadata. + Assert.True(asset.IsImage); + Assert.Equal(300, asset.PixelHeight); + Assert.Equal(300, asset.PixelWidth); + Assert.Equal(300L, asset.Metadata["pixelHeight"]); + Assert.Equal(300L, asset.Metadata["pixelWidth"]); + Assert.Equal(96L, asset.Metadata["imageQuality"]); + Assert.Equal(AssetType.Image, asset.Type); + } + + [Fact] + public async Task Should_upload_audio_mp3() + { + var asset = await _.UploadFileAsync("Assets/SampleAudio_0.4mb.mp3", "audio/mp3"); + + // Should parse audio metadata. + Assert.False(asset.IsImage); + Assert.Equal("00:00:28.2708750", asset.Metadata["duration"]); + Assert.Equal(128L, asset.Metadata["audioBitrate"]); + Assert.Equal(2L, asset.Metadata["audioChannels"]); + Assert.Equal(44100L, asset.Metadata["audioSampleRate"]); + Assert.Equal(AssetType.Audio, asset.Type); + } + + [Fact] + public async Task Should_upload_video_mp4() + { + var asset = await _.UploadFileAsync("Assets/SampleVideo_1280x720_1mb.mp4", "audio/mp4"); + + // Should parse video metadata. + Assert.False(asset.IsImage); + Assert.Equal("00:00:05.3120000", asset.Metadata["duration"]); + Assert.Equal(384L, asset.Metadata["audioBitrate"]); + Assert.Equal(2L, asset.Metadata["audioChannels"]); + Assert.Equal(48000L, asset.Metadata["audioSampleRate"]); + Assert.Equal(1280L, asset.Metadata["videoWidth"]); + Assert.Equal(720L, asset.Metadata["videoHeight"]); + Assert.Equal(AssetType.Video, asset.Type); + } + + [Fact] + public async Task Should_upload_video_mkv() + { + var asset = await _.UploadFileAsync("Assets/SampleVideo_1280x720_1mb.flv", "audio/webm"); + + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); + } + + [Fact] + public async Task Should_upload_video_flv() + { + var asset = await _.UploadFileAsync("Assets/SampleVideo_1280x720_1mb.flv", "audio/x-flv"); + + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); + } + + [Fact] + public async Task Should_upload_video_3gp() + { + var asset = await _.UploadFileAsync("Assets/SampleVideo_176x144_1mb.3gp", "audio/3gpp"); + + // Should not parse yet. + Assert.Equal(AssetType.Unknown, asset.Type); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs new file mode 100644 index 000000000..6a1f9170d --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -0,0 +1,157 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; +using TestSuite.Fixtures; +using Xunit; + +#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 AssetTests : IClassFixture + { + public AssetFixture _ { get; } + + public AssetTests(AssetFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_replace_asset() + { + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + + + // STEP 2: Reupload asset + var asset_2 = await _.UploadFileAsync("Assets/logo-wide.png", asset_1); + + 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); + } + } + + [Fact] + public async Task Should_annote_asset() + { + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + + + // STEP 2: Annotate metadata. + var metadataRequest = new AnnotateAssetDto + { + Metadata = new Dictionary + { + ["pw"] = 100L, + ["ph"] = 20L + } + }; + + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id.ToString(), metadataRequest); + + // Should provide metadata. + Assert.Equal(metadataRequest.Metadata, asset_2.Metadata); + + + // STEP 3: Annotate slug. + var slugRequest = new AnnotateAssetDto { Slug = "my-image" }; + + var asset_3 = await _.Assets.PutAssetAsync(_.AppName, asset_2.Id.ToString(), slugRequest); + + // Should provide updated slug. + Assert.Equal(slugRequest.Slug, asset_3.Slug); + + + // STEP 3: Annotate file name. + var fileNameRequest = new AnnotateAssetDto { FileName = "My Image" }; + + var asset_4 = await _.Assets.PutAssetAsync(_.AppName, asset_3.Id.ToString(), fileNameRequest); + + // Should provide updated file name. + Assert.Equal(fileNameRequest.FileName, asset_4.FileName); + } + + [Fact] + public async Task Should_protect_asset() + { + var fileName = $"{Guid.NewGuid()}.png"; + + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + + + // STEP 2: Download asset + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var downloaded = await _.DownloadAsync(asset_1); + + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); + } + + + // STEP 4: Protect asset + var protectRequest = new AnnotateAssetDto { IsProtected = true }; + + var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id.ToString(), protectRequest); + + + // STEP 5: Download asset with authentication. + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var downloaded = new MemoryStream(); + + using (var assetStream = await _.Assets.GetAssetContentAsync(asset_2.Id.ToString())) + { + await assetStream.Stream.CopyToAsync(downloaded); + } + + // Should dowload with correct size. + Assert.Equal(stream.Length, downloaded.Length); + } + + + // STEP 5: Download asset without key. + using (var stream = new FileStream("Assets/logo-squared.png", FileMode.Open)) + { + var ex = await Assert.ThrowsAsync(() => _.DownloadAsync(asset_1)); + + // Should return 403 when not authenticated. + Assert.Contains("403", ex.Message); + } + } + + [Fact] + public async Task Should_delete_asset() + { + // STEP 1: Create asset + var asset_1 = await _.UploadFileAsync("Assets/logo-squared.png", "image/png"); + + + // STEP 2: Delete asset + await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id.ToString()); + + // Should return 404 when asset deleted. + var ex = await Assert.ThrowsAsync(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id.ToString())); + + Assert.Equal(404, ex.StatusCode); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleAudio_0.4mb.mp3 b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleAudio_0.4mb.mp3 new file mode 100644 index 000000000..084a7d1f8 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleAudio_0.4mb.mp3 differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleGIFImage_40kbmb.gif b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleGIFImage_40kbmb.gif new file mode 100644 index 000000000..d41b6a157 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleGIFImage_40kbmb.gif differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleJPGImage_50kbmb.jpg b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleJPGImage_50kbmb.jpg new file mode 100644 index 000000000..3a52a95df Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleJPGImage_50kbmb.jpg differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SamplePNGImage_100kbmb.png b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SamplePNGImage_100kbmb.png new file mode 100644 index 000000000..22392090a Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SamplePNGImage_100kbmb.png differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.flv b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.flv new file mode 100644 index 000000000..32b43bfc6 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.flv differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mkv b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mkv new file mode 100644 index 000000000..3fa72285a Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mkv differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mp4 b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mp4 new file mode 100644 index 000000000..ed139d6d5 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_1280x720_1mb.mp4 differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_176x144_1mb.3gp b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_176x144_1mb.3gp new file mode 100644 index 000000000..7b2e319e9 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/SampleVideo_176x144_1mb.3gp differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-squared.png b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-squared.png new file mode 100644 index 000000000..3cbc19038 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-squared.png differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide.png b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide.png new file mode 100644 index 000000000..9a29fa803 Binary files /dev/null and b/backend/tools/TestSuite/TestSuite.ApiTests/Assets/logo-wide.png differ diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs new file mode 100644 index 000000000..8c6da95c4 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/BackupTests.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; +using TestSuite.Fixtures; +using Xunit; + +#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 BackupTests : IClassFixture + { + public ClientFixture _ { get; } + + public BackupTests(ClientFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_backup_and_restore_app() + { + var timeout = TimeSpan.FromMinutes(2); + + var appName = Guid.NewGuid().ToString(); + var appNameRestore = $"{appName}-restore"; + + // STEP 1: Create app + var createRequest = new CreateAppDto { Name = appName }; + + await _.Apps.PostAppAsync(createRequest); + + + // STEP 2: Create backup + await _.Backups.PostBackupAsync(appName); + + BackupJobDto backup = null; + + try + { + using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2))) + { + while (true) + { + cts.Token.ThrowIfCancellationRequested(); + + await Task.Delay(1000); + + var backups = await _.Backups.GetBackupsAsync(appName); + + if (backups.Items.Count > 0) + { + backup = backups.Items.FirstOrDefault(); + + break; + } + } + } + } + catch (OperationCanceledException) + { + Assert.True(false, $"Could not retrieve backup within {timeout}."); + } + + + // STEP 3: Restore backup + var uri = new Uri($"{_.ServerUrl}{backup._links["download"].Href}"); + + var restoreRequest = new RestoreRequestDto { Url = uri, Name = appNameRestore }; + + await _.Backups.PostRestoreJobAsync(restoreRequest); + + try + { + using (var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2))) + { + while (true) + { + cts.Token.ThrowIfCancellationRequested(); + + await Task.Delay(1000); + + var job = await _.Backups.GetRestoreJobAsync(); + + if (job != null && job.Url == uri && job.Status == JobStatus.Completed) + { + break; + } + } + } + } + catch (OperationCanceledException) + { + Assert.True(false, $"Could not retrieve restored app within {timeout}."); + } + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs new file mode 100644 index 000000000..1ad8eb3b8 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -0,0 +1,258 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.ClientLibrary; +using TestSuite.Fixtures; +using TestSuite.Model; +using Xunit; + +#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 ContentQueryTests : IClassFixture + { + public ContentQueryFixture _ { get; } + + public ContentQueryTests(ContentQueryFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_query_by_ids() + { + var items = await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" }); + + var itemsById = await _.Contents.GetAsync(new HashSet(items.Items.Take(3).Select(x => x.EntityId))); + + Assert.Equal(3, itemsById.Items.Count); + Assert.Equal(3, itemsById.Total); + + foreach (var item in itemsById.Items) + { + Assert.Equal(_.AppName, item.AppName); + Assert.Equal(_.SchemaName, item.SchemaName); + } + } + + [Fact] + public async Task Should_return_all_with_odata() + { + var items = await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" }); + + AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + + [Fact] + public async Task Should_return_all_with_json() + { + var items = await _.Contents.GetAsync(new ContentQuery + { + JsonQuery = new + { + sort = new[] + { + new + { + path = "data.number.iv", order = "ascending" + } + } + } + }); + + AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + + [Fact] + public async Task Should_return_items_with_skip_with_odata() + { + var items = await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/number/iv asc" }); + + AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); + } + + [Fact] + public async Task Should_return_items_with_skip_with_json() + { + var items = await _.Contents.GetAsync(new ContentQuery + { + JsonQuery = new + { + sort = new[] + { + new + { + path = "data.number.iv", order = "ascending" + } + }, + skip = 5 + } + }); + + AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); + } + + [Fact] + public async Task Should_return_items_with_skip_and_top_with_odata() + { + var items = await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }); + + AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); + } + + [Fact] + public async Task Should_return_items_with_skip_and_top_with_json() + { + var items = await _.Contents.GetAsync(new ContentQuery + { + JsonQuery = new + { + sort = new[] + { + new + { + path = "data.number.iv", order = "ascending" + } + }, + skip = 2, top = 5 + } + }); + + AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); + } + + [Fact] + public async Task Should_return_items_with_filter_with_odata() + { + var items = await _.Contents.GetAsync(new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }); + + AssertItems(items, 3, new[] { 4, 5, 6 }); + } + + [Fact] + public async Task Should_return_items_with_filter_with_json() + { + var items = await _.Contents.GetAsync(new ContentQuery + { + JsonQuery = new + { + sort = new[] + { + new + { + path = "data.number.iv", order = "ascending" + } + }, + filter = new + { + and = new[] + { + new + { + path = "data.number.iv", + op = "gt", + value = 3 + }, + new + { + path = "data.number.iv", + op = "lt", + value = 7 + } + } + } + } + }); + + AssertItems(items, 3, new[] { 4, 5, 6 }); + } + + + [Fact] + public async Task Should_query_items_with_graphql() + { + var query = new + { + query = @" + { + queryMyReadsContents(filter: ""data/number/iv gt 3 and data/number/iv lt 7"", orderby: ""data/number/iv asc"") { + id, + data { + number { + iv + } + } + } + }" + }; + + var result = await _.Contents.GraphQlAsync(query); + + var items = result.Items; + + Assert.Equal(items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + } + + [Fact] + public async Task Should_query_items_with_graphql_with_dynamic() + { + var query = new + { + query = @" + { + queryMyReadsContents(filter: ""data/number/iv gt 3 and data/number/iv lt 7"", orderby: ""data/number/iv asc"") { + id, + data { + number { + iv + } + } + } + }" + }; + + var result = await _.Contents.GraphQlAsync(query); + + var items = result["queryMyReadsContents"]; + + Assert.Equal(items.Select(x => x["data"]["number"]["iv"].Value()).ToArray(), new[] { 4, 5, 6 }); + } + + private sealed class QueryResult + { + [JsonProperty("queryMyReadsContents")] + public QueryItem[] Items { get; set; } + } + + private sealed class QueryItem + { + public Guid Id { get; set; } + + public QueryItemData Data { get; set; } + } + + private sealed class QueryItemData + { + [JsonConverter(typeof(InvariantConverter))] + public int Number { get; set; } + } + + private void AssertItems(SquidexEntities entities, int total, int[] expected) + { + Assert.Equal(total, entities.Total); + Assert.Equal(expected, entities.Items.Select(x => x.Data.Number).ToArray()); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs new file mode 100644 index 000000000..9e0005739 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -0,0 +1,210 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.ClientLibrary; +using TestSuite.Fixtures; +using TestSuite.Model; +using Xunit; + +#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 ContentUpdateTests : IClassFixture + { + public ContentFixture _ { get; } + + public ContentUpdateTests(ContentFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_create_strange_text() + { + const string text = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"; + + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { String = text }, true); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(text, updated.Data.String); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_not_return_not_published_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_return_item_published_with_creation() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + await _.Contents.GetAsync(content.Id); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_return_item_published_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + await _.Contents.ChangeStatusAsync(content.Id, Status.Published); + await _.Contents.GetAsync(content.Id); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_not_return_archived_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + await _.Contents.ChangeStatusAsync(content.Id, Status.Archived); + + await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_not_return_unpublished_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }); + + await _.Contents.ChangeStatusAsync(content.Id, Status.Published); + await _.Contents.ChangeStatusAsync(content.Id, Status.Draft); + + await Assert.ThrowsAsync(() => _.Contents.GetAsync(content.Id)); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_update_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); + + await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 }); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, updated.Data.Number); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_patch_item() + { + TestEntity content = null; + try + { + content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true); + + await _.Contents.PatchAsync(content.Id, new TestEntityData { Number = 2 }); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, updated.Data.Number); + } + finally + { + if (content.Id != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_delete_item() + { + var content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, true); + + await _.Contents.DeleteAsync(content.Id); + + var updated = await _.Contents.GetAsync(); + + Assert.DoesNotContain(updated.Items, x => x.Id == content.Id); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs new file mode 100644 index 000000000..5aaf19cf3 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/LanguagesTests.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using TestSuite.Fixtures; +using Xunit; + +#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 LanguagesTests : IClassFixture + { + public ClientFixture _ { get; } + + public LanguagesTests(ClientFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_provide_languages() + { + var languages = await _.Languages.GetLanguagesAsync(); + + Assert.True(languages.Count > 100); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs new file mode 100644 index 000000000..0f95ce63f --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/PingTests.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using TestSuite.Fixtures; +using Xunit; + +#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 PingTests : IClassFixture + { + public CreatedAppFixture _ { get; } + + public PingTests(CreatedAppFixture fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_ping_service() + { + await _.Ping.GetPingAsync(); + } + + [Fact] + public async Task Should_ping_app() + { + await _.Ping.GetAppPingAsync(_.AppName); + } + + [Fact] + public async Task Should_get_info() + { + var infos = await _.Ping.GetInfoAsync(); + + Assert.NotNull(infos); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/Setup.cs b/backend/tools/TestSuite/TestSuite.ApiTests/Setup.cs new file mode 100644 index 000000000..a9d911028 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/Setup.cs @@ -0,0 +1,10 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)] \ No newline at end of file diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj new file mode 100644 index 000000000..ce21788bc --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -0,0 +1,60 @@ + + + Exe + netcoreapp3.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + ..\..\..\Squidex.ruleset + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs new file mode 100644 index 000000000..ecfa0ee6a --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingBenchmarks.cs @@ -0,0 +1,71 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using TestSuite.Fixtures; +using Xunit; + +#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.LoadTests +{ + public class ReadingBenchmarks : IClassFixture + { + public CreatedAppFixture _ { get; } + + public ReadingBenchmarks(CreatedAppFixture fixture) + { + _ = fixture; + } + + public static IEnumerable Loads() + { + int[] users = + { + 1, + 5, + 10, + 20, + 50, + 100 + }; + + int[] loads = + { + 1, + 5, + 10, + 20, + 50, + 100, + 1000 + }; + + foreach (var user in users) + { + foreach (var load in loads) + { + yield return new object[] { user, load }; + } + } + + yield return new object[] { 1, 20000 }; + } + + [Theory] + [MemberData(nameof(Loads))] + public async Task Should_return_clients(int numUsers, int numIterationsPerUser) + { + await Run.Parallel(numUsers, numIterationsPerUser, async () => + { + await _.Apps.GetClientsAsync(_.AppName); + }); + } + } +} diff --git a/backend/tools/LoadTest/ReadingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs similarity index 68% rename from backend/tools/LoadTest/ReadingBenchmarks.cs rename to backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs index 115628271..8fe47b336 100644 --- a/backend/tools/LoadTest/ReadingBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingContentBenchmarks.cs @@ -7,19 +7,21 @@ using System.Collections.Generic; using System.Threading.Tasks; -using LoadTest.Model; using Squidex.ClientLibrary; using Xunit; -namespace LoadTest +#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.LoadTests { - public class ReadingBenchmarks : IClassFixture + public class ReadingContentBenchmarks : IClassFixture { - public ReadingFixture Fixture { get; } + public ReadingFixture _ { get; } - public ReadingBenchmarks(ReadingFixture fixture) + public ReadingContentBenchmarks(ReadingFixture fixture) { - Fixture = fixture; + _ = fixture; } public static IEnumerable Loads() @@ -62,7 +64,7 @@ namespace LoadTest { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.GetAsync(new ODataQuery { OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/value/iv asc" }); }); } @@ -72,7 +74,7 @@ namespace LoadTest { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.GetAsync(new ODataQuery { Skip = 5, OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 5, OrderBy = "data/value/iv asc" }); }); } @@ -82,7 +84,7 @@ namespace LoadTest { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.GetAsync(new ODataQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv asc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv asc" }); }); } @@ -92,7 +94,7 @@ namespace LoadTest { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.GetAsync(new ODataQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv desc" }); + await _.Contents.GetAsync(new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/value/iv desc" }); }); } @@ -102,17 +104,7 @@ namespace LoadTest { await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.GetAsync(new ODataQuery { Filter = "data/value/iv gt 3 and data/value/iv lt 7", OrderBy = "data/value/iv asc" }); - }); - } - - [Theory] - [MemberData(nameof(Loads))] - public async Task Should_return_clients(int numUsers, int numIterationsPerUser) - { - await Run.Parallel(numUsers, numIterationsPerUser, async () => - { - await Fixture.AppsClient.GetClientsAsync(TestClient.TestAppName); + await _.Contents.GetAsync(new ContentQuery { Filter = "data/value/iv gt 3 and data/value/iv lt 7", OrderBy = "data/value/iv asc" }); }); } } diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs new file mode 100644 index 000000000..d1765c6a3 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.LoadTests/ReadingFixture.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using TestSuite.Fixtures; + +namespace TestSuite.LoadTests +{ + public sealed class ReadingFixture : ContentFixture + { + public ReadingFixture() + : base("benchmark_reading") + { + } + } +} diff --git a/backend/tools/LoadTest/Run.cs b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs similarity index 98% rename from backend/tools/LoadTest/Run.cs rename to backend/tools/TestSuite/TestSuite.LoadTests/Run.cs index a430350f5..f3d421d6a 100644 --- a/backend/tools/LoadTest/Run.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/Run.cs @@ -14,7 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -namespace LoadTest +namespace TestSuite.LoadTests { public static class Run { diff --git a/backend/tools/LoadTest/LoadTest.csproj b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj similarity index 72% rename from backend/tools/LoadTest/LoadTest.csproj rename to backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index 5f3b3d534..68052186d 100644 --- a/backend/tools/LoadTest/LoadTest.csproj +++ b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -1,4 +1,4 @@ - + Exe netcoreapp3.0 @@ -6,7 +6,6 @@ - @@ -15,9 +14,12 @@ - ..\..\Squidex.ruleset + ..\..\..\Squidex.ruleset - + + + + diff --git a/backend/tools/LoadTest/WritingBenchmarks.cs b/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs similarity index 80% rename from backend/tools/LoadTest/WritingBenchmarks.cs rename to backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs index af45c9ca1..c2d7cab06 100644 --- a/backend/tools/LoadTest/WritingBenchmarks.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/WritingBenchmarks.cs @@ -8,18 +8,21 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using LoadTest.Model; +using TestSuite.Model; using Xunit; -namespace LoadTest +#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.LoadTests { public class WritingBenchmarks : IClassFixture { - public WritingFixture Fixture { get; } + public WritingFixture _ { get; } public WritingBenchmarks(WritingFixture fixture) { - Fixture = fixture; + _ = fixture; } public static IEnumerable Loads() @@ -62,7 +65,7 @@ namespace LoadTest await Run.Parallel(numUsers, numIterationsPerUser, async () => { - await Fixture.Client.CreateAsync(new TestEntityData { Number = random.Next() }, true); + await _.Contents.CreateAsync(new TestEntityData { Number = random.Next() }, true); }); } } diff --git a/backend/tools/LoadTest/WritingFixture.cs b/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs similarity index 50% rename from backend/tools/LoadTest/WritingFixture.cs rename to backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs index b1a2ffe3a..1fc527a32 100644 --- a/backend/tools/LoadTest/WritingFixture.cs +++ b/backend/tools/TestSuite/TestSuite.LoadTests/WritingFixture.cs @@ -5,26 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Threading.Tasks; -using LoadTest.Model; -using Squidex.ClientLibrary; +using TestSuite.Fixtures; -namespace LoadTest +namespace TestSuite.LoadTests { - public sealed class WritingFixture : IDisposable + public sealed class WritingFixture : ContentFixture { - public SquidexClient Client { get; private set; } - public WritingFixture() - { - Task.Run(async () => - { - Client = await TestClient.BuildAsync("writing"); - }).Wait(); - } - - public void Dispose() + : base("benchmark_writing") { } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs new file mode 100644 index 000000000..be68f6d95 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; + +namespace TestSuite.Fixtures +{ + public class AssetFixture : CreatedAppFixture + { + public IAssetsClient Assets { get; } + + public AssetFixture() + { + Assets = ClientManager.CreateAssetsClient(); + } + + public async Task DownloadAsync(AssetDto asset) + { + var temp = new MemoryStream(); + + using (var client = new HttpClient()) + { + var url = $"{ServerUrl}{asset._links["content"].Href}"; + + var response = await client.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + using (var stream = await response.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(temp); + } + } + + return temp; + } + + public async Task UploadFileAsync(string path, AssetDto asset, string fileName = null) + { + var fileInfo = new FileInfo(path); + + using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), asset.MimeType); + + return await Assets.PutAssetContentAsync(AppName, asset.Id.ToString(), upload); + } + } + + public async Task UploadFileAsync(string path, string mimeType, string fileName = null) + { + var fileInfo = new FileInfo(path); + + using (var stream = fileInfo.OpenRead()) + { + var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), mimeType); + + return await Assets.PostAssetAsync(AppName, upload); + } + } + + private static string RandomName(FileInfo fileInfo) + { + var fileName = $"{Guid.NewGuid()}{fileInfo.Extension}"; + + return fileName; + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs new file mode 100644 index 000000000..a618adc78 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientFixture.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.ClientLibrary.Management; + +namespace TestSuite.Fixtures +{ + public class ClientFixture : ClientManagerFixture + { + public IAppsClient Apps => ClientManager.CreateAppsClient(); + + public IBackupsClient Backups => ClientManager.CreateBackupsClient(); + + public ILanguagesClient Languages => ClientManager.CreateLanguagesClient(); + + public IPingClient Ping => ClientManager.CreatePingClient(); + + public IRulesClient Rules => ClientManager.CreateRulesClient(); + + public ISchemasClient Schemas => ClientManager.CreateSchemasClient(); + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs new file mode 100644 index 000000000..901567c7c --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ClientManagerFixture.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Squidex.ClientLibrary; + +namespace TestSuite.Fixtures +{ + public class ClientManagerFixture : IDisposable + { + public string ServerUrl { get; } = "http://localhost:5000"; + + public string ClientId { get; } = "root"; + + public string ClientSecret { get; } = "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="; + + public string AppName { get; } = "integration-tests"; + + public SquidexClientManager ClientManager { get; } + + public ClientManagerFixture() + { + ClientManager = new SquidexClientManager(ServerUrl, AppName, ClientId, ClientSecret) + { + ReadResponseAsString = true + }; + } + + public virtual void Dispose() + { + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs new file mode 100644 index 000000000..5572478fa --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentFixture.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; +using TestSuite.Model; + +namespace TestSuite.Fixtures +{ + public class ContentFixture : CreatedAppFixture + { + public SquidexClient Contents { get; } + + public string SchemaName { get; } + + public string FieldNumber { get; } = "number"; + + public string FieldString { get; } = "string"; + + public ContentFixture() + : this("my-writes") + { + } + + protected ContentFixture(string schemaName) + { + SchemaName = schemaName; + + Task.Run(async () => + { + try + { + var schemas = ClientManager.CreateSchemasClient(); + + await schemas.PostSchemaAsync(AppName, new CreateSchemaDto + { + Name = SchemaName, + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = FieldNumber, + Properties = new NumberFieldPropertiesDto + { + IsRequired = true + } + }, + new UpsertSchemaFieldDto + { + Name = FieldString, + Properties = new StringFieldPropertiesDto + { + IsRequired = false + } + } + }, + IsPublished = true + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + }).Wait(); + + Contents = ClientManager.GetClient(SchemaName); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs new file mode 100644 index 000000000..4e8399769 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentQueryFixture.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.ClientLibrary; +using TestSuite.Model; + +namespace TestSuite.Fixtures +{ + public class ContentQueryFixture : ContentFixture + { + public ContentQueryFixture() + : this("my-reads") + { + } + + protected ContentQueryFixture(string schemaName = "my-schema") + : base(schemaName) + { + Task.Run(async () => + { + Dispose(); + + for (var i = 10; i > 0; i--) + { + await Contents.CreateAsync(new TestEntityData { Number = i }, true); + } + }).Wait(); + } + + public override void Dispose() + { + Task.Run(async () => + { + var contents = await Contents.GetAllAsync(); + + foreach (var content in contents.Items) + { + await Contents.DeleteAsync(content); + } + }).Wait(); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs new file mode 100644 index 000000000..322d04b7c --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/CreatedAppFixture.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.ClientLibrary.Management; + +namespace TestSuite.Fixtures +{ + public class CreatedAppFixture : ClientFixture + { + public CreatedAppFixture() + { + Task.Run(async () => + { + try + { + await Apps.PostAppAsync(new CreateAppDto { Name = AppName }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + + await Apps.PostContributorAsync(AppName, new AssignContributorDto { ContributorId = "sebastian@squidex.io", Invite = true, Role = "Owner" }); + }).Wait(); + } + } +} diff --git a/backend/tools/LoadTest/Model/TestEntity.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs similarity index 96% rename from backend/tools/LoadTest/Model/TestEntity.cs rename to backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs index b96ed2975..d34abf4be 100644 --- a/backend/tools/LoadTest/Model/TestEntity.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Model/TestEntity.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; using Squidex.ClientLibrary; -namespace LoadTest.Model +namespace TestSuite.Model { public sealed class TestEntity : SquidexEntityBase { diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj new file mode 100644 index 000000000..bf06d6c6d --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -0,0 +1,18 @@ + + + netcoreapp3.0 + + + + + + + + + ..\..\..\Squidex.ruleset + TestSuite + + + + + diff --git a/backend/tools/LoadTest/Utils/RandomHash.cs b/backend/tools/TestSuite/TestSuite.Shared/Utils/RandomHash.cs similarity index 98% rename from backend/tools/LoadTest/Utils/RandomHash.cs rename to backend/tools/TestSuite/TestSuite.Shared/Utils/RandomHash.cs index 9dd29bd62..60045423b 100644 --- a/backend/tools/LoadTest/Utils/RandomHash.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Utils/RandomHash.cs @@ -9,7 +9,7 @@ using System; using System.Security.Cryptography; using System.Text; -namespace LoadTest.Utils +namespace TestSuite.Utils { public static class RandomHash { diff --git a/backend/tools/LoadTest/Utils/RandomString.cs b/backend/tools/TestSuite/TestSuite.Shared/Utils/RandomString.cs similarity index 96% rename from backend/tools/LoadTest/Utils/RandomString.cs rename to backend/tools/TestSuite/TestSuite.Shared/Utils/RandomString.cs index 1f626cb1e..cce99b45c 100644 --- a/backend/tools/LoadTest/Utils/RandomString.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Utils/RandomString.cs @@ -7,7 +7,7 @@ using System; -namespace LoadTest.Utils +namespace TestSuite.Utils { public static class RandomString { diff --git a/backend/tools/LoadTest/Utils/Run.cs b/backend/tools/TestSuite/TestSuite.Shared/Utils/Run.cs similarity index 98% rename from backend/tools/LoadTest/Utils/Run.cs rename to backend/tools/TestSuite/TestSuite.Shared/Utils/Run.cs index 17187a4f8..e3fb0d959 100644 --- a/backend/tools/LoadTest/Utils/Run.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Utils/Run.cs @@ -14,7 +14,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -namespace LoadTest.Utils +namespace TestSuite.Utils { public static class Run { diff --git a/backend/tools/TestSuite/TestSuite.sln b/backend/tools/TestSuite/TestSuite.sln new file mode 100644 index 000000000..8bc1f5817 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.Shared", "TestSuite.Shared\TestSuite.Shared.csproj", "{37484845-5542-4E52-AB00-C4576B84FE75}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.ApiTests", "TestSuite.ApiTests\TestSuite.ApiTests.csproj", "{E5F048CB-5307-4E4C-8DAB-2F1C0E5CACF3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestSuite.LoadTests", "TestSuite.LoadTests\TestSuite.LoadTests.csproj", "{F37572D9-4880-40F4-B3CB-83F58A40CA48}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {37484845-5542-4E52-AB00-C4576B84FE75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37484845-5542-4E52-AB00-C4576B84FE75}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37484845-5542-4E52-AB00-C4576B84FE75}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37484845-5542-4E52-AB00-C4576B84FE75}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F048CB-5307-4E4C-8DAB-2F1C0E5CACF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F048CB-5307-4E4C-8DAB-2F1C0E5CACF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F048CB-5307-4E4C-8DAB-2F1C0E5CACF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F048CB-5307-4E4C-8DAB-2F1C0E5CACF3}.Release|Any CPU.Build.0 = Release|Any CPU + {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F37572D9-4880-40F4-B3CB-83F58A40CA48}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9F1CDBED-7D91-4B46-B4C5-0FE086E29285} + EndGlobalSection +EndGlobal diff --git a/frontend/app/features/apps/pages/apps-page.component.html b/frontend/app/features/apps/pages/apps-page.component.html index 756f92c1e..491995974 100644 --- a/frontend/app/features/apps/pages/apps-page.component.html +++ b/frontend/app/features/apps/pages/apps-page.component.html @@ -20,7 +20,7 @@
-
+

{{app.displayName}}

-
\ No newline at end of file +