From cac87d84f6e89fdd6a243618f7e9a2432011fed1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 23 Nov 2022 17:09:06 +0100 Subject: [PATCH] Fix referencing queries and improve tests. (#942) * Fix referencing queries and improve tests. * Another test. * Tests fixed. --- .../Assets/MongoAssetRepository.cs | 13 +- .../Contents/MongoContentCollection.cs | 14 +- .../Contents/Operations/Extensions.cs | 2 +- .../Contents/Operations/QueryByIds.cs | 24 +- .../Contents/Operations/QueryByQuery.cs | 8 +- .../Operations/QueryInDedicatedCollection.cs | 4 +- .../TestSuite.ApiTests/AssetTests.cs | 161 +++++----- .../TestSuite.ApiTests/ContentFixture.cs | 2 +- .../TestSuite.ApiTests/ContentQueryFixture.cs | 5 +- .../TestSuite.ApiTests/ContentQueryTests.cs | 8 +- .../TestSuite.ApiTests/GraphQLFixture.cs | 195 ++++++++++++ .../TestSuite.ApiTests/GraphQLTests.cs | 277 ++++++++---------- .../TestSuite.ApiTests.csproj | 10 +- .../TestSuite.LoadTests.csproj | 4 +- .../TestSuite.Shared/Model/Geography.cs | 44 +++ .../TestSuite.Shared/TestSuite.Shared.csproj | 16 +- 16 files changed, 486 insertions(+), 301 deletions(-) create mode 100644 backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs create mode 100644 backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 7e8e2ee30..d4b2bc53e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -89,6 +89,9 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase 0 }) { var filter = BuildFilter(appId, q.Ids.ToHashSet()); @@ -98,10 +101,10 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase x.LastModified).ThenBy(x => x.Id) .QueryLimit(q.Query) .QuerySkip(q.Query) - .ToListRandomAsync(Collection, q.Query.Random, ct); + .ToListRandomAsync(Collection, query.Random, ct); long assetTotal = assetEntities.Count; - if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) + if (assetEntities.Count >= query.Take || query.Skip > 0) { if (q.NoTotal) { @@ -117,8 +120,6 @@ public sealed partial class MongoAssetRepository : MongoRepositoryBase= q.Query.Take || q.Query.Skip > 0) + if (assetEntities.Count >= query.Take || query.Skip > 0) { - var isDefaultQuery = q.Query.Filter == null; + var isDefaultQuery = query.Filter == null; if (q.NoTotal || (q.NoSlowTotal && !isDefaultQuery)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 8bf3c7f79..d0067f2be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -180,17 +180,17 @@ public sealed class MongoContentCollection : MongoRepositoryBase { schema }, q, ct); } - if (q.Referencing == default) + if (q.Referencing != default) { - if (queryInDedicatedCollection != null) - { - return await queryInDedicatedCollection.QueryAsync(schema, q, ct); - } + return await queryReferences.QueryAsync(app, new List { schema }, q, ct); + } - return await queryByQuery.QueryAsync(schema, q, ct); + if (queryInDedicatedCollection != null) + { + return await queryInDedicatedCollection.QueryAsync(schema, q, ct); } - return ResultList.Empty(); + return await queryByQuery.QueryAsync(schema, q, ct); } catch (MongoCommandException ex) when (ex.Code == 96) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs index 20a764f46..cbe813b90 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/Extensions.cs @@ -106,8 +106,8 @@ public static class Extensions var result = collection.Find(filter) .QuerySort(query) - .QueryLimit(query) .QuerySkip(query) + .QueryLimit(query) .ToListRandomAsync(collection, query.Random, ct); return await result; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs index 6fff3f624..3168ee434 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs @@ -28,7 +28,8 @@ internal sealed class QueryByIds : OperationBase return ReadonlyList.Empty(); } - var filter = CreateFilter(appId, null, ids); + // Create a filter from the Ids and ensure that the content ids match to the app ID. + var filter = CreateFilter(appId, null, ids, null); var contentEntities = await Collection.FindStatusAsync(filter, ct); @@ -43,12 +44,16 @@ internal sealed class QueryByIds : OperationBase return ResultList.Empty(); } - var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet()); + // We need to translate the query names to the document field names in MongoDB. + var query = q.Query.AdjustToModel(app.Id); - var contentEntities = await FindContentsAsync(q.Query, filter, ct); + // Create a filter from the Ids and ensure that the content ids match to the schema IDs. + var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet(), query.Filter); + + var contentEntities = await FindContentsAsync(query, filter, ct); var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= query.Take || query.Skip > 0) { if (q.NoTotal) { @@ -68,14 +73,16 @@ internal sealed class QueryByIds : OperationBase { var result = Collection.Find(filter) - .QueryLimit(query) + .QuerySort(query) .QuerySkip(query) + .QueryLimit(query) .ToListRandomAsync(Collection, query.Random, ct); return await result; } - private static FilterDefinition CreateFilter(DomainId appId, IEnumerable? schemaIds, HashSet ids) + private static FilterDefinition CreateFilter(DomainId appId, IEnumerable? schemaIds, HashSet ids, + FilterNode? filter) { var filters = new List>(); @@ -101,6 +108,11 @@ internal sealed class QueryByIds : OperationBase filters.Add(Filter.Ne(x => x.IsDeleted, true)); + if (filter != null) + { + filters.Add(filter.BuildFilter()); + } + return Filter.And(filters); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index df2901be2..f0ddbb441 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -66,9 +66,9 @@ internal sealed class QueryByQuery : OperationBase var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= query.Take || query.Skip > 0) { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) + if (q.NoTotal || (q.NoSlowTotal && query.Filter != null)) { contentTotal = -1; } @@ -98,9 +98,9 @@ internal sealed class QueryByQuery : OperationBase var contentEntities = await Collection.QueryContentsAsync(filter, query, ct); var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= query.Take || query.Skip > 0) { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) + if (q.NoTotal || (q.NoSlowTotal && query.Filter != null)) { contentTotal = -1; } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs index ba853375f..d810e913d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryInDedicatedCollection.cs @@ -91,9 +91,9 @@ internal sealed class QueryInDedicatedCollection : MongoBase var contentEntities = await contentCollection.QueryContentsAsync(filter, query, ct); var contentTotal = (long)contentEntities.Count; - if (contentTotal >= q.Query.Take || q.Query.Skip > 0) + if (contentTotal >= query.Take || query.Skip > 0) { - if (q.NoTotal || (q.NoSlowTotal && q.Query.Filter != null)) + if (q.NoTotal || (q.NoSlowTotal && query.Filter != null)) { contentTotal = -1; } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index fb3821823..5ac338422 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -6,11 +6,9 @@ // ========================================================================== using System.Net; -using System.Runtime.Intrinsics.X86; using Squidex.Assets; using Squidex.ClientLibrary.Management; using TestSuite.Fixtures; -using Xunit.Sdk; #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 @@ -80,44 +78,12 @@ public class AssetTests : IClassFixture var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); - var pausingStream = new PauseStream(fileParameter.Data, 0.25); - var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); - - var numUploads = 0; - var numConflicts = 0; - - await using (pausingFile.Data) - { - using var cts = new CancellationTokenSource(5000); - - while (progress.Asset == null) - { - // When the previous request is still in progress we just give it another try. - if (progress.Exception is SquidexManagementException { StatusCode: 409 } && numConflicts < 3) - { - numConflicts++; - - progress.ResetException(); - - // Wait a little bit to finish the request on the server. - await Task.Delay(100, cts.Token); - } - else if (progress.Exception != null) - { - break; - } - - pausingStream.Reset(); - - await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(), cts.Token); - numUploads++; - } - } + await UploadInChunksAsync(fileParameter); Assert.Null(progress.Exception); Assert.NotEmpty(progress.Progress); Assert.NotNull(progress.Asset); - Assert.True(numUploads > 1); + Assert.True(progress.Uploads.Count > 1); await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) { @@ -242,7 +208,7 @@ public class AssetTests : IClassFixture [Fact] public async Task Should_replace_asset_using_tus_in_chunks() { - for (var i = 0; i < 5; i++) + for (var i = 0; i < 1; i++) { // STEP 1: Create asset var asset_1 = await _.Assets.UploadFileAsync(_.AppName, "Assets/logo-squared.png", "image/png"); @@ -253,45 +219,12 @@ public class AssetTests : IClassFixture var fileParameter = FileParameter.FromPath("Assets/SampleVideo_1280x720_1mb.mp4"); - var pausingStream = new PauseStream(fileParameter.Data, 0.25); - var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType); - - var numUploads = 0; - var numConflicts = 0; - - await using (pausingFile.Data) - { - using var cts = new CancellationTokenSource(5000); - - // When the previous request is still in progress we just give it another try. - while (progress.Asset == null) - { - // When the previous request is still in progress we just give it another try. - if (progress.Exception is SquidexManagementException { StatusCode: 409 } && numConflicts < 3) - { - numConflicts++; - - progress.ResetException(); - - // Wait a little bit to finish the request on the server. - await Task.Delay(100, cts.Token); - } - else if (progress.Exception != null) - { - break; - } - - pausingStream.Reset(); - - await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(asset_1.Id), cts.Token); - numUploads++; - } - } + await UploadInChunksAsync(fileParameter, asset_1.Id); Assert.Null(progress.Exception); Assert.NotEmpty(progress.Progress); Assert.NotNull(progress.Asset); - Assert.True(numUploads > 1); + Assert.True(progress.Uploads.Count > 1); await using (var stream = new FileStream("Assets/SampleVideo_1280x720_1mb.mp4", FileMode.Open)) { @@ -699,12 +632,36 @@ public class AssetTests : IClassFixture Assert.NotEqual(asset_1.FileSize, asset_2.FileSize); } + private async Task UploadInChunksAsync(FileParameter fileParameter, string id = null) + { + var pausingStream = new PauseStream(fileParameter.Data, 0.25); + var pausingFile = new FileParameter(pausingStream, fileParameter.FileName, fileParameter.ContentType) + { + ContentLength = fileParameter.Data.Length + }; + + await using (pausingFile.Data) + { + using var cts = new CancellationTokenSource(5000); + + while (progress.Asset == null && progress.Exception == null && !cts.IsCancellationRequested) + { + pausingStream.Reset(); + + await _.Assets.UploadAssetAsync(_.AppName, pausingFile, progress.AsOptions(id), cts.Token); + progress.Uploaded(); + } + } + } + public class ProgressHandler : IAssetProgressHandler { - public string FileId { get; private set; } + public string FileId { get; private set; } = Guid.NewGuid().ToString(); public List Progress { get; } = new List(); + public List Uploads { get; } = new List(); + public Exception Exception { get; private set; } public AssetDto Asset { get; private set; } @@ -719,17 +676,14 @@ public class AssetTests : IClassFixture return options; } - public void ResetException() + public void Uploaded() { - Exception = null; + Uploads.Add(Progress.LastOrDefault()); } public Task OnCompletedAsync(AssetUploadCompletedEvent @event, CancellationToken ct) { - // This is a previous exception, so we can unset it. - ResetException(); - Asset = @event.Asset; return Task.CompletedTask; } @@ -751,28 +705,44 @@ public class AssetTests : IClassFixture public Task OnFailedAsync(AssetUploadExceptionEvent @event, CancellationToken ct) { - if (@event.Exception.InnerException is not PauseException) - { - Exception = @event.Exception; - } - + Exception = @event.Exception; return Task.CompletedTask; } } - private sealed class PauseException : Exception + public class PauseStream : DelegateStream { - } + private readonly int maxLength; + private long totalRead; + private long totalRemaining; + private long seekStart; - private sealed class PauseStream : DelegateStream - { - private readonly double pauseAfter = 1; - private int totalRead; + public override long Length + { + get => Math.Min(maxLength, totalRemaining); + } + + public override long Position + { + get => base.Position - seekStart; + set => throw new NotSupportedException(); + } public PauseStream(Stream innerStream, double pauseAfter) : base(innerStream) { - this.pauseAfter = pauseAfter; + maxLength = (int)Math.Floor(innerStream.Length * pauseAfter) + 1; + + totalRemaining = innerStream.Length; + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = seekStart = base.Seek(offset, origin); + + totalRemaining = base.Length - position; + + return position; } public void Reset() @@ -783,9 +753,16 @@ public class AssetTests : IClassFixture public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { - if (totalRead > Length * pauseAfter) + var remaining = Length - totalRead; + + if (remaining <= 0) + { + return 0; + } + + if (remaining < buffer.Length) { - throw new PauseException(); + buffer = buffer[..(int)remaining]; } var bytesRead = await base.ReadAsync(buffer, cancellationToken); diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs index b4103e996..46bd67d02 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentFixture.cs @@ -9,7 +9,7 @@ using TestSuite.Fixtures; namespace TestSuite.ApiTests; -public sealed class ContentFixture : TestSchemaFixtureBase +public class ContentFixture : TestSchemaFixtureBase { public ContentFixture() : base("my-writes") diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs index ca3862bf4..c0d582b12 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryFixture.cs @@ -38,7 +38,10 @@ public sealed class ContentQueryFixture : TestSchemaFixtureBase nested2 = index } }), - Geo = GeoJson.Point(index, index, oldFormat: index % 2 == 1), + Geo = GeoJson.Point( + index + 100, + index, + oldFormat: index % 2 == 1), Localized = new Dictionary { ["en"] = index.ToString(CultureInfo.InvariantCulture) diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index b8d43cb39..9f7b63422 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -361,7 +361,7 @@ public class ContentQueryTests : IClassFixture [Fact] public async Task Should_query_by_near_location_with_odata() { - var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" }; + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(103 3)') lt 1000" }; var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); @@ -381,7 +381,7 @@ public class ContentQueryTests : IClassFixture op = "lt", value = new { - longitude = 3, + longitude = 103, latitude = 3, distance = 1000 } @@ -397,7 +397,7 @@ public class ContentQueryTests : IClassFixture [Fact] public async Task Should_query_by_near_geoson_location_with_odata() { - var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" }; + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(104 4)') lt 1000" }; var items = await _.Contents.WaitForContentAsync(q, x => true, TimeSpan.FromSeconds(30)); @@ -466,7 +466,7 @@ public class ContentQueryTests : IClassFixture op = "lt", value = new { - longitude = 4, + longitude = 104, latitude = 4, distance = 1000 } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs new file mode 100644 index 000000000..0e6eb1a88 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLFixture.cs @@ -0,0 +1,195 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; + +#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row + +namespace TestSuite.ApiTests; + +public sealed class GraphQLFixture : ContentFixture +{ + public sealed class DynamicEntity : Content + { + } + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + + await CreateSchemasAsync(); + await CreateContentsAsync(); + } + + private async Task CreateSchemasAsync() + { + async Task CreateSchemaAsync(CreateSchemaDto request) + { + try + { + var response = await Schemas.PostSchemaAsync(AppName, request); + + return response.Id; + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + + var schema = await Schemas.GetSchemaAsync(AppName, request.Name); + + return schema.Id; + } + } + + // STEP 1: Create cities schema. + var createCitiesRequest = new CreateSchemaDto + { + Name = "cities", + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = "name", + Properties = new StringFieldPropertiesDto() + } + }, + IsPublished = true + }; + + var citiesId = await CreateSchemaAsync(createCitiesRequest); + + + // STEP 2: Create states schema. + var createStatesRequest = new CreateSchemaDto + { + Name = "states", + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = "name", + Properties = new StringFieldPropertiesDto() + }, + new UpsertSchemaFieldDto + { + Name = "cities", + Properties = new ReferencesFieldPropertiesDto + { + SchemaIds = new List { citiesId } + } + } + }, + IsPublished = true + }; + + var statesId = await CreateSchemaAsync(createStatesRequest); + + + // STEP 3: Create countries schema. + var createCountriesRequest = new CreateSchemaDto + { + Name = "countries", + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = "name", + Properties = new StringFieldPropertiesDto() + }, + new UpsertSchemaFieldDto + { + Name = "states", + Properties = new ReferencesFieldPropertiesDto + { + SchemaIds = new List { statesId } + } + } + }, + IsPublished = true + }; + + await CreateSchemaAsync(createCountriesRequest); + } + + private async Task CreateContentsAsync() + { + var countriesClient = ClientManager.CreateContentsClient("countries"); + var countriesResponse = await countriesClient.GetAsync(); + + if (countriesResponse.Total > 0) + { + return; + } + + async Task CreateCityAsync(string name) + { + var citySAData = new + { + name = new + { + iv = name + } + }; + + var citiesClient = ClientManager.CreateContentsClient("cities"); + + var city = await citiesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish); + + return city.Id; + } + + async Task CreateStateAsync(string name, string cityId) + { + var citySAData = new + { + name = new + { + iv = name + }, + cities = new + { + iv = new[] { cityId } + } + }; + + var statesClient = ClientManager.CreateContentsClient("states"); + + var state = await statesClient.CreateAsync(citySAData, ContentCreateOptions.AsPublish); + + return state.Id; + } + + // STEP 1: Create state 1 + var sachsenCapital = await CreateCityAsync("Leipzig"); + var sachstenState = await CreateStateAsync("Sachsen", sachsenCapital); + + + // STEP 1: Create state 2 + var badenWCapital = await CreateCityAsync("Stuttgart"); + var badenWState = await CreateStateAsync("Baden Württemberg", badenWCapital); + + + // STEP 3: Create country + var countryData = new + { + name = new + { + iv = "Germany" + }, + states = new + { + iv = new[] { sachstenState, badenWState } + } + }; + + await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish); + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs index b9ef3008b..5a2dd76cb 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json.Linq; using Squidex.ClientLibrary; -using Squidex.ClientLibrary.Management; using TestSuite.Model; #pragma warning disable SA1300 // Element should begin with upper-case letter @@ -15,53 +14,15 @@ using TestSuite.Model; namespace TestSuite.ApiTests; -public sealed class GraphQLTests : IClassFixture +public sealed class GraphQLTests : IClassFixture { - public ContentFixture _ { get; } + public GraphQLFixture _ { get; } - public GraphQLTests(ContentFixture fixture) + public GraphQLTests(GraphQLFixture fixture) { _ = fixture; } - public sealed class DynamicEntity : Content - { - } - - public sealed class Country - { - public CountryData Data { get; set; } - } - - public sealed class CountryData - { - public string Name { get; set; } - - public List States { get; set; } - } - - public sealed class State - { - public StateData Data { get; set; } - } - - public sealed class StateData - { - public string Name { get; set; } - - public List Cities { get; set; } - } - - public sealed class City - { - public CityData Data { get; set; } - } - - public sealed class CityData - { - public string Name { get; set; } - } - [Fact] public async Task Should_query_json() { @@ -92,38 +53,20 @@ public sealed class GraphQLTests : IClassFixture }".Replace("", content_0.Id, StringComparison.Ordinal) }; - var result1 = await _.SharedContents.GraphQlAsync(query); + var result = await _.SharedContents.GraphQlAsync(query); - Assert.Equal(1, result1["findMyWritesContent"]["flatData"]["json"]["value"].Value()); - Assert.Equal(2, result1["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value()); + Assert.Equal(1, result["findMyWritesContent"]["flatData"]["json"]["value"].Value()); + Assert.Equal(2, result["findMyWritesContent"]["flatData"]["json"]["obj"]["value"].Value()); } [Fact] - public async Task Should_create_and_query_with_graphql() + public async Task Should_query_graphql_reference_selectors() { - try - { - await CreateSchemasAsync(); - } - catch - { - // Do nothing - } - - try - { - await CreateContentsAsync(); - } - catch - { - // Do nothing - } - var query = new { query = @" { - queryCountriesContents { + countries: queryCountriesContents { data: flatData { name, states { @@ -141,134 +84,144 @@ public sealed class GraphQLTests : IClassFixture }" }; - var result1 = await _.SharedContents.GraphQlAsync(query); + var result = await _.SharedContents.GraphQlAsync(query); - var typed = result1["queryCountriesContents"].ToObject>(); + var cityNames = + result["countries"].ToObject>()[0].Data.States + .SelectMany(x => x.Data.Cities) + .Select(x => x.Data.Name) + .Order(); - Assert.Equal("Leipzig", typed[0].Data.States[0].Data.Cities[0].Data.Name); + Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames); } - private async Task CreateSchemasAsync() + [Fact] + public async Task Should_query_graphql_reference_operator() { - // STEP 1: Create cities schema. - var createCitiesRequest = new CreateSchemaDto + var query = new { - Name = "cities", - Fields = new List - { - new UpsertSchemaFieldDto + query = @" { - Name = "name", - Properties = new StringFieldPropertiesDto() - } - }, - IsPublished = true + countries: queryCountriesContents { + data: flatData { + name, + states { + data: flatData { + name + }, + cities: referencesCitiesContents { + data: flatData { + name + } + } + } + } + } + }" }; - var cities = await _.Schemas.PostSchemaAsync(_.AppName, createCitiesRequest); + var result = await _.SharedContents.GraphQlAsync(query); + + var cityNames = + result["countries"] + .SelectMany(x => x["data"]["states"]) + .SelectMany(x => x["cities"]) + .Select(x => x["data"]["name"].Value()) + .Order(); + Assert.Equal(new[] { "Leipzig", "Stuttgart" }, cityNames); + } - // STEP 2: Create states schema. - var createStatesRequest = new CreateSchemaDto + [Fact] + public async Task Should_query_graphql_reference_operator_with_filter() + { + var query = new { - Name = "states", - Fields = new List - { - new UpsertSchemaFieldDto - { - Name = "name", - Properties = new StringFieldPropertiesDto() - }, - new UpsertSchemaFieldDto + query = @" { - Name = "cities", - Properties = new ReferencesFieldPropertiesDto - { - SchemaIds = new List { cities.Id } + countries: queryCountriesContents { + data: flatData { + name, + states { + data: flatData { + name + }, + cities: referencesCitiesContents(filter: ""data/name/iv eq 'Leipzig'"") { + data: flatData { + name + } + } + } + } } - } - }, - IsPublished = true + }" }; - var states = await _.Schemas.PostSchemaAsync(_.AppName, createStatesRequest); - + var result = await _.SharedContents.GraphQlAsync(query); - // STEP 3: Create countries schema. - var createCountriesRequest = new CreateSchemaDto - { - Name = "countries", - Fields = new List - { - new UpsertSchemaFieldDto - { - Name = "name", - Properties = new StringFieldPropertiesDto() - }, - new UpsertSchemaFieldDto - { - Name = "states", - Properties = new ReferencesFieldPropertiesDto - { - SchemaIds = new List { states.Id } - } - } - }, - IsPublished = true - }; + var cityNames = + result["countries"] + .SelectMany(x => x["data"]["states"]) + .SelectMany(x => x["cities"]) + .Select(x => x["data"]["name"].Value()) + .Order(); - await _.Schemas.PostSchemaAsync(_.AppName, createCountriesRequest); + Assert.Equal(new[] { "Leipzig" }, cityNames); } - private async Task CreateContentsAsync() + [Fact] + public async Task Should_query_graphql_referencing_operator() { - // STEP 1: Create city - var cityData = new + var query = new { - name = new - { - iv = "Leipzig" - } + query = @" + { + cities: queryCitiesContents { + states: referencingStatesContents { + data: flatData { + name + } + } + } + }" }; - var citiesClient = _.ClientManager.CreateContentsClient("cities"); + var result = await _.SharedContents.GraphQlAsync(query); - var city = await citiesClient.CreateAsync(cityData, ContentCreateOptions.AsPublish); + var stateNames = + result["cities"] + .SelectMany(x => x["states"]) + .Select(x => x["data"]["name"].Value()) + .Order(); + Assert.Equal(new[] { "Baden Württemberg", "Sachsen" }, stateNames); + } - // STEP 2: Create city - var stateData = new + [Fact] + public async Task Should_query_graphql_referencing_operator_with_filter() + { + var query = new { - name = new - { - iv = "Saxony" - }, - cities = new - { - iv = new[] { city.Id } - } + query = @" + { + cities: queryCitiesContents { + states: referencingStatesContents(filter: ""data/name/iv eq 'Sachsen'"") { + data: flatData { + name + } + } + } + }" }; - var statesClient = _.ClientManager.CreateContentsClient("states"); - - var state = await statesClient.CreateAsync(stateData, ContentCreateOptions.AsPublish); - - - // STEP 3: Create country - var countryData = new - { - name = new - { - iv = "Germany" - }, - states = new - { - iv = new[] { state.Id } - } - }; + var result = await _.SharedContents.GraphQlAsync(query); - var countriesClient = _.ClientManager.CreateContentsClient("countries"); + var stateNames = + result["cities"] + .SelectMany(x => x["states"]) + .Select(x => x["data"]["name"].Value()) + .Order(); - await countriesClient.CreateAsync(countryData, ContentCreateOptions.AsPublish); + Assert.Equal(new[] { "Sachsen" }, stateNames); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index fe017df41..3704b228d 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -15,14 +15,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index 939c9be40..a26900858 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj +++ b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -6,11 +6,11 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs b/backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs new file mode 100644 index 000000000..ca5a91d1d --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Model/Geography.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +#pragma warning disable MA0048 // File name must match type name + +namespace TestSuite.Model; + +public sealed class Country +{ + public CountryData Data { get; set; } +} + +public sealed class CountryData +{ + public string Name { get; set; } + + public List States { get; set; } +} + +public sealed class State +{ + public StateData Data { get; set; } +} + +public sealed class StateData +{ + public string Name { get; set; } + + public List Cities { get; set; } +} + +public sealed class City +{ + public CityData Data { get; set; } +} + +public sealed class CityData +{ + public string Name { get; set; } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index 99da4fce3..deebcbee1 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -6,18 +6,18 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + - - + +