diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs index cd4003449..0ff16f3a6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -123,11 +123,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds void AddValue(object value) { - if (sb.Length > 0) - { - sb.Append(separator); - } - + sb.AppendIfNotEmpty(separator); sb.Append(value); } 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 018757f8c..d71b7f7fe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -94,10 +94,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets var filter = BuildFilter(appId, q.Ids.ToHashSet()); var assetEntities = - await Collection.Find(filter).SortByDescending(x => x.LastModified).ThenBy(x => x.Id) + await Collection.Find(filter) + .SortByDescending(x => x.LastModified).ThenBy(x => x.Id) .QueryLimit(q.Query) .QuerySkip(q.Query) - .ToListAsync(ct); + .ToListRandomAsync(Collection, q.Query.Random, ct); long assetTotal = assetEntities.Count; if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) @@ -126,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets .QueryLimit(query) .QuerySkip(query) .QuerySort(query) - .ToListAsync(ct); + .ToListRandomAsync(Collection, query.Random, ct); long assetTotal = assetEntities.Count; if (assetEntities.Count >= q.Query.Take || q.Query.Skip > 0) 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 62379d476..77d64fdab 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 @@ -9,6 +9,7 @@ using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; @@ -55,8 +56,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations query.Sort[1].Order == SortOrder.Ascending; } - public static async Task> QueryContentsAsync(this IMongoCollection collection, - FilterDefinition filter, ClrQuery query, + public static async Task> QueryContentsAsync(this IMongoCollection collection, FilterDefinition filter, ClrQuery query, CancellationToken ct) { if (query.Skip > 0 && !query.IsSatisfiedByIndex()) @@ -70,6 +70,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations projection = projection.Include(field); } + if (query.Random > 0) + { + var ids = + await collection.Aggregate() + .Match(filter) + .Project(projection) + .QuerySort(query) + .QuerySkip(query) + .QueryLimit(query) + .ToListAsync(ct); + + var randomIds = ids.Select(x => x.Id).TakeRandom(query.Random); + + var documents = + await collection.Find(Builders.Filter.In(x => x.Id, randomIds)) + .ToListAsync(ct); + + return documents.Shuffle().ToList(); + } + var joined = await collection.Aggregate() .Match(filter) @@ -88,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations .QuerySort(query) .QueryLimit(query) .QuerySkip(query) - .ToListAsync(ct); + .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 9af0a212e..0f384b5cd 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 @@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.Queries; @@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations var filter = CreateFilter(app.Id, schemas.Select(x => x.Id), q.Ids.ToHashSet()); - var contentEntities = await FindContentsAsync(q.Query, filter); + var contentEntities = await FindContentsAsync(q.Query, filter, ct); var contentTotal = (long)contentEntities.Count; if (contentTotal >= q.Query.Take || q.Query.Skip > 0) @@ -62,13 +63,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return ResultList.Create(contentTotal, contentEntities); } - private async Task> FindContentsAsync(ClrQuery query, FilterDefinition filter) + private async Task> FindContentsAsync(ClrQuery query, FilterDefinition filter, + CancellationToken ct) { var result = Collection.Find(filter) .QueryLimit(query) .QuerySkip(query) - .ToListAsync(); + .ToListRandomAsync(Collection, query.Random, ct); return await result; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index e7f2b728f..4ed662d1a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -99,11 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { if (!string.IsNullOrWhiteSpace(text)) { - if (sb.Length > 0) - { - sb.Append(", "); - } - + sb.AppendIfNotEmpty(", "); sb.Append(text); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs index 30192a804..25bac531e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs @@ -125,11 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents if (!string.IsNullOrWhiteSpace(formatted)) { - if (sb.Length > 0) - { - sb.Append(", "); - } - + sb.AppendIfNotEmpty(", "); sb.Append(formatted); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs index df4939aea..e0052cdb6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs @@ -8,6 +8,7 @@ using System.Text; using NetTopologySuite.Geometries; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.ObjectPool; @@ -116,11 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text languages[language] = sb; } - if (sb.Length > 0) - { - sb.Append(' '); - } - + sb.AppendIfNotEmpty(' '); sb.Append(text); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index a759e9e0e..9e6b4473c 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -217,5 +217,41 @@ namespace Squidex.Infrastructure.MongoDb return result; } + + public static async Task> ToListRandomAsync(this IFindFluent find, IMongoCollection collection, long take, + CancellationToken ct = default) + { + if (take <= 0) + { + return await find.ToListAsync(ct); + } + + var idDocuments = await find.Project(Builders.Projection.Include("_id")).ToListAsync(ct); + var idValues = idDocuments.Select(x => x["_id"]); + + var randomIds = idValues.TakeRandom(take); + + var documents = await collection.Find(Builders.Filter.In("_id", randomIds)).ToListAsync(ct); + + return documents.Shuffle().ToList(); + } + + public static async Task> ToListRandomAsync(this IAggregateFluent find, IMongoCollection collection, long take, + CancellationToken ct = default) + { + if (take <= 0) + { + return await find.ToListAsync(ct); + } + + var idDocuments = await find.Project(Builders.Projection.Include("_id")).ToListAsync(ct); + var idValues = idDocuments.Select(x => x["_id"]); + + var randomIds = idValues.TakeRandom(take); + + var documents = await collection.Find(Builders.Filter.In("_id", randomIds)).ToListAsync(ct); + + return documents.Shuffle().ToList(); + } } } diff --git a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs index 501342d03..e0f15ec15 100644 --- a/backend/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/backend/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -98,11 +98,7 @@ namespace Squidex.Infrastructure foreach (var item in source) { - if (bucket == null) - { - bucket = new TSource[size]; - } - + bucket ??= new TSource[size]; bucket[bucketIndex++] = item; if (bucketIndex != size) @@ -179,9 +175,7 @@ namespace Squidex.Infrastructure public static IEnumerable Shuffle(this IEnumerable enumerable) { - var random = new Random(); - - return enumerable.OrderBy(x => random.Next()).ToList(); + return enumerable.OrderBy(x => Random.Shared.Next()).ToList(); } public static IEnumerable OrEmpty(this IEnumerable? source) @@ -389,5 +383,44 @@ namespace Squidex.Infrastructure index++; } } + + public static IEnumerable TakeRandom(this IEnumerable source, long take) + { + var sourceList = new LinkedList(source); + + static LinkedListNode TakeNode(LinkedList source, int index) + { + var actual = source.First!; + + for (var i = 0; i < index; i++) + { + var next = actual?.Next; + + if (next == null) + { + return actual!; + } + + actual = next; + } + + return actual; + } + + for (var i = 0; i < take; i++) + { + if (sourceList.Count == 0) + { + break; + } + + var takenIndex = Random.Shared.Next(sourceList.Count); + var takenElement = TakeNode(sourceList, takenIndex); + + sourceList.Remove(takenElement); + + yield return takenElement.Value; + } + } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 64e68c192..58a5050bb 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -69,6 +69,7 @@ namespace Squidex.Infrastructure.Queries.OData parser.ParseSkip(query); parser.ParseFilter(query); parser.ParseSort(query); + parser.ParseRandom(query); } return query; diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs index 10d36cfba..02068ddeb 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/LimitExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using Microsoft.OData.UriParser; namespace Squidex.Infrastructure.Queries.OData @@ -30,5 +31,21 @@ namespace Squidex.Infrastructure.Queries.OData result.Skip = skip.Value; } } + + public static void ParseRandom(this ODataUriParser query, ClrQuery result) + { + var customQueries = query.CustomQueryOptions; + + var randomQuery = customQueries.FirstOrDefault(x => + string.Equals(x.Key, "random", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "randomCount", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "$random", StringComparison.OrdinalIgnoreCase) || + string.Equals(x.Key, "$randomCount", StringComparison.OrdinalIgnoreCase)); + + if (int.TryParse(randomQuery.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var random)) + { + result.Random = random; + } + } } } diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs index 3529cfc24..66b549680 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Query.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Text; + namespace Squidex.Infrastructure.Queries { public class Query @@ -17,6 +19,8 @@ namespace Squidex.Infrastructure.Queries public long Take { get; set; } = long.MaxValue; + public long Random { get; set; } + public long Top { set => Take = value; @@ -43,34 +47,45 @@ namespace Squidex.Infrastructure.Queries public override string ToString() { - var parts = new List(); + var sb = new StringBuilder(); if (Filter != null) { - parts.Add($"Filter: {Filter}"); + sb.AppendIfNotEmpty("; "); + sb.Append($"Filter: {Filter}"); } if (FullText != null) { - parts.Add($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'"); + sb.AppendIfNotEmpty("; "); + sb.Append($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'"); } if (Skip > 0) { - parts.Add($"Skip: {Skip}"); + sb.AppendIfNotEmpty("; "); + sb.Append($"Skip: {Skip}"); } if (Take < long.MaxValue) { - parts.Add($"Take: {Take}"); + sb.AppendIfNotEmpty("; "); + sb.Append($"Take: {Take}"); + } + + if (Random > 0) + { + sb.AppendIfNotEmpty("; "); + sb.Append($"Random: {Random}"); } if (Sort != null && Sort.Count > 0) { - parts.Add($"Sort: {string.Join(", ", Sort)}"); + sb.AppendIfNotEmpty("; "); + sb.Append($"Sort: {string.Join(", ", Sort)}"); } - return string.Join("; ", parts); + return sb.ToString(); } } } diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index 97ab3ce12..3c97c6fb7 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Globalization; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; @@ -114,5 +115,25 @@ namespace Squidex.Infrastructure return new string(span); } + + public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, char separator) + { + if (sb.Length > 0) + { + sb.Append(separator); + } + + return sb; + } + + public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, string separator) + { + if (sb.Length > 0) + { + sb.Append(separator); + } + + return sb; + } } } diff --git a/backend/src/Squidex.Web/ExposedValues.cs b/backend/src/Squidex.Web/ExposedValues.cs index c227144b8..64b80d0af 100644 --- a/backend/src/Squidex.Web/ExposedValues.cs +++ b/backend/src/Squidex.Web/ExposedValues.cs @@ -48,11 +48,7 @@ namespace Squidex.Web foreach (var (key, value) in this) { - if (sb.Length > 0) - { - sb.Append(", "); - } - + sb.AppendIfNotEmpty(", "); sb.Append(key); sb.Append(": "); sb.Append(value); diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index 01035ee9f..adab239e4 100644 --- a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; using System.Text; namespace Squidex.Pipeline.Squid @@ -97,11 +98,7 @@ namespace Squidex.Pipeline.Squid line.Clear(); } - if (line.Length > 0) - { - line.Append(' '); - } - + line.AppendIfNotEmpty(' '); line.Append(word); } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 516807666..baaddf005 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -54,7 +54,6 @@ - diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index 869572725..8478b4eed 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -24,7 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb { public sealed class AssetsQueryFixture : IAsyncLifetime { - private readonly Random random = new Random(); private readonly int numValues = 250; private readonly IMongoClient mongoClient; private readonly IMongoDatabase mongoDatabase; @@ -103,8 +102,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var asset = new AssetDomainObject.State { - Tags = new HashSet { tag }, Id = DomainId.NewGuid(), + AppId = appId, Created = now, CreatedBy = user, FileHash = fileName, @@ -118,6 +117,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb { ["value"] = JsonValue.Create(tag) }, + Tags = new HashSet + { + tag + }, Slug = fileName }; @@ -139,12 +142,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb public DomainId RandomAppId() { - return AppIds[random.Next(0, AppIds.Length)].Id; + return AppIds[Random.Shared.Next(AppIds.Length)].Id; } public string RandomValue() { - return random.Next(0, numValues).ToString(CultureInfo.InvariantCulture); + return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs index a387cb3bb..4918cf158 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryIntegrationTests.cs @@ -31,6 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var asset = await _.AssetRepository.FindAssetBySlugAsync(_.RandomAppId(), random); + // The Slug is random here, as it does not really matter. Assert.NotNull(asset); } @@ -41,6 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await _.AssetRepository.FindAssetByHashAsync(_.RandomAppId(), random, random, 1024); + // The Hash is random here, as it does not really matter. Assert.NotNull(assets); } @@ -51,18 +53,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await _.AssetRepository.QueryIdsAsync(_.RandomAppId(), ids); + // The IDs are random here, as it does not really matter. Assert.NotNull(assets); } [Theory] [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_default(DomainId? parentId) + public async Task Should_query_assets(DomainId? parentId) { var query = new ClrQuery(); var assets = await QueryAsync(parentId, query); - Assert.NotNull(assets); + // Default page size is 1000. + Assert.Equal(1000, assets.Count); + } + + [Theory] + [MemberData(nameof(ParentIds))] + public async Task Should_query_assets_with_random_count(DomainId? parentId) + { + var query = new ClrQuery + { + Random = 40 + }; + + var assets = await QueryAsync(parentId, query); + + // Default page size is 1000, so we expect less elements. + Assert.Equal(40, assets.Count); } [Theory] @@ -78,12 +97,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await QueryAsync(parentId, query); + // The tag is random here, as it does not really matter. Assert.NotNull(assets); } [Theory] [MemberData(nameof(ParentIds))] - public async Task Should_query_assets_by_tags_and_name(DomainId? parentId) + public async Task Should_query_assets_by_tags_and_fileName(DomainId? parentId) { var random = _.RandomValue(); @@ -94,7 +114,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await QueryAsync(parentId, query); - Assert.NotNull(assets); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); } [Theory] @@ -110,7 +131,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await QueryAsync(parentId, query); - Assert.NotNull(assets); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); } [Theory] @@ -126,7 +148,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb var assets = await QueryAsync(parentId, query); - Assert.NotNull(assets); + // The filter is a random value from the expected result set. + Assert.NotEmpty(assets); } public static IEnumerable ParentIds() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index 99c4f9b50..2958e5d0a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -163,7 +163,6 @@ namespace Squidex.Domain.Apps.Entities.Backup [InlineData(BackupVersion.V2)] public async Task Should_write_and_read_events_to_backup(BackupVersion version) { - var randomGenerator = new Random(); var randomDomainIds = new List(); for (var i = 0; i < 100; i++) @@ -173,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Backup DomainId RandomDomainId() { - return randomDomainIds[randomGenerator.Next(randomDomainIds.Count)]; + return randomDomainIds[Random.Shared.Next(randomDomainIds.Count)]; } var sourceEvents = new List<(string Stream, Envelope Event)>(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs new file mode 100644 index 000000000..1074defec --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryDedicatedIntegrationTests.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Xunit; + +#pragma warning disable SA1300 // Element should begin with upper-case letter +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +{ + [Trait("Category", "Dependencies")] + public class ContentsQueryDedicatedIntegrationTests : ContentsQueryTestsBase, IClassFixture + { + public ContentsQueryDedicatedIntegrationTests(ContentsQueryDedicatedFixture fixture) + : base(fixture) + { + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index 8d0574b61..d3ec8f641 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -49,7 +49,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public abstract class ContentsQueryFixtureBase : IAsyncLifetime { - private readonly Random random = new Random(); private readonly int numValues = 10000; private readonly IMongoClient mongoClient; private readonly IMongoDatabase mongoDatabase; @@ -196,15 +195,22 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var appProvider = A.Fake(); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, A._)) - .ReturnsLazily(x => Task.FromResult(CreateSchema(x.GetArgument(0)!, x.GetArgument(1)!))); + A.CallTo(() => appProvider.GetAppWithSchemaAsync(A._, A._, false, A._)) + .ReturnsLazily(x => + { + var appId = x.GetArgument(0)!; + + return Task.FromResult<(IAppEntity?, ISchemaEntity?)>(( + CreateApp(appId), + CreateSchema(appId, x.GetArgument(1)!))); + }); return appProvider; } public DomainId RandomAppId() { - return AppIds[random.Next(0, AppIds.Length)].Id; + return AppIds[Random.Shared.Next(AppIds.Length)].Id; } public IAppEntity RandomApp() @@ -214,7 +220,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public DomainId RandomSchemaId() { - return SchemaIds[random.Next(0, SchemaIds.Length)].Id; + return SchemaIds[Random.Shared.Next(SchemaIds.Length)].Id; } public ISchemaEntity RandomSchema() @@ -224,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb public string RandomValue() { - return random.Next(0, numValues).ToString(CultureInfo.InvariantCulture); + return Random.Shared.Next(numValues).ToString(CultureInfo.InvariantCulture); } private static IAppEntity CreateApp(DomainId appId) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs index ec62a7b1f..ed6054813 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs @@ -5,16 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using NodaTime; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Queries; using Xunit; -using F = Squidex.Infrastructure.Queries.ClrFilter; - -#pragma warning disable SA1300 // Element should begin with upper-case letter -#pragma warning disable MA0048 // File name must match type name namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { @@ -26,214 +17,4 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { } } - - [Trait("Category", "Dependencies")] - public class ContentsQueryDedicatedIntegrationTests : ContentsQueryTestsBase, IClassFixture - { - public ContentsQueryDedicatedIntegrationTests(ContentsQueryDedicatedFixture fixture) - : base(fixture) - { - } - } - - public abstract class ContentsQueryTestsBase - { - public ContentsQueryFixtureBase _ { get; } - - protected ContentsQueryTestsBase(ContentsQueryFixtureBase fixture) - { - _ = fixture; - } - - [Fact] - public async Task Should_verify_ids() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); - - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_by_ids() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - - var schemas = new List - { - _.RandomSchema() - }; - - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); - - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_by_ids_and_schema() - { - var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); - - var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); - - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_ids_by_filter() - { - var filter = F.Eq("data.field1.iv", 12); - - var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); - - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } - - [Fact] - public async Task Should_query_contents_by_filter() - { - var query = new ClrQuery - { - Filter = F.Eq("data.field1.iv", 12) - }; - - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } - - [Fact] - public async Task Should_query_contents_scheduled() - { - var time = SystemClock.Instance.GetCurrentInstant(); - - var contents = await _.ContentRepository.QueryScheduledWithoutDataAsync(time).ToListAsync(); - - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_with_default_query() - { - var query = new ClrQuery(); - - var contents = await QueryAsync(_.ContentRepository, query); - - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } - - [Fact] - public async Task Should_query_contents_with_default_query_and_id() - { - var query = new ClrQuery(); - - var contents = await QueryAsync(_.ContentRepository, query, reference: DomainId.NewGuid()); - - // The IDs are random here, as it does not really matter. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_with_large_skip() - { - var query = new ClrQuery - { - Sort = new List - { - new SortNode("data.value.iv", SortOrder.Ascending) - } - }; - - var contents = await QueryAsync(_.ContentRepository, query, 1000, 9000); - - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } - - [Fact] - public async Task Should_query_contents_with_query_fulltext() - { - var query = new ClrQuery - { - FullText = "hello" - }; - - var contents = await QueryAsync(_.ContentRepository, query); - - // The full text is resolved by another system, so we cannot verify the actual. - Assert.NotNull(contents); - } - - [Fact] - public async Task Should_query_contents_with_query_filter() - { - var query = new ClrQuery - { - Filter = F.Eq("data.field1.iv", 200) - }; - - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); - - // We have a concrete query, so we expect an actual. - Assert.NotEmpty(contents); - } - - [Fact] - public async Task Should_query_contents_with_query_filter_and_id() - { - var query = new ClrQuery - { - Filter = F.Eq("data.value.iv", 12) - }; - - var contents = await QueryAsync(_.ContentRepository, query, 1000, 0, reference: DomainId.NewGuid()); - - // We do not insert test entities with references, so we cannot verify the actual. - Assert.Empty(contents); - } - - private async Task> QueryAsync(IContentRepository contentRepository, - ClrQuery clrQuery, - int take = 1000, - int skip = 100, - DomainId reference = default) - { - if (clrQuery.Take == long.MaxValue) - { - clrQuery.Take = take; - } - - if (clrQuery.Skip == 0) - { - clrQuery.Skip = skip; - } - - if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) - { - clrQuery.Sort = new List - { - new SortNode("LastModified", SortOrder.Descending), - new SortNode("Id", SortOrder.Ascending) - }; - } - - var q = - Q.Empty - .WithoutTotal() - .WithQuery(clrQuery) - .WithReference(reference); - - var contents = await contentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); - - return contents; - } - } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs new file mode 100644 index 000000000..cecc08e3f --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryTestsBase.cs @@ -0,0 +1,235 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Queries; +using Xunit; +using F = Squidex.Infrastructure.Queries.ClrFilter; + +#pragma warning disable SA1300 // Element should begin with upper-case letter +#pragma warning disable MA0048 // File name must match type name + +namespace Squidex.Domain.Apps.Entities.Contents.MongoDb +{ + public abstract class ContentsQueryTestsBase + { + public ContentsQueryFixtureBase _ { get; } + + protected ContentsQueryTestsBase(ContentsQueryFixtureBase fixture) + { + _ = fixture; + } + + [Fact] + public async Task Should_verify_ids() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + + var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), ids, SearchScope.Published); + + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_by_ids() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + + var schemas = new List + { + _.RandomSchema() + }; + + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), schemas, Q.Empty.WithIds(ids), SearchScope.All); + + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_by_ids_and_schema() + { + var ids = Enumerable.Repeat(0, 50).Select(_ => DomainId.NewGuid()).ToHashSet(); + + var contents = await _.ContentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), Q.Empty.WithIds(ids), SearchScope.All); + + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_ids_by_filter() + { + var filter = F.Eq("data.field1.iv", 12); + + var contents = await _.ContentRepository.QueryIdsAsync(_.RandomAppId(), _.RandomSchemaId(), filter); + + // We have a concrete query, so we expect an actual. + Assert.NotEmpty(contents); + } + + [Fact] + public async Task Should_query_contents_by_filter() + { + var query = new ClrQuery + { + Filter = F.Eq("data.field1.iv", 12) + }; + + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); + + // We have a concrete query, so we expect an actual. + Assert.NotEmpty(contents); + } + + [Fact] + public async Task Should_query_contents_scheduled() + { + var time = SystemClock.Instance.GetCurrentInstant(); + + var contents = await _.ContentRepository.QueryScheduledWithoutDataAsync(time).ToListAsync(); + + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_with_default_query() + { + var query = new ClrQuery(); + + var contents = await QueryAsync(_.ContentRepository, query); + + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } + + [Fact] + public async Task Should_query_contents_with_default_query_and_id() + { + var query = new ClrQuery(); + + var contents = await QueryAsync(_.ContentRepository, query, reference: DomainId.NewGuid()); + + // The IDs are random here, as it does not really matter. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_with_large_skip() + { + var query = new ClrQuery + { + Sort = new List + { + new SortNode("data.value.iv", SortOrder.Ascending) + } + }; + + var contents = await QueryAsync(_.ContentRepository, query, 1000, 9000); + + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } + + [Fact] + public async Task Should_query_contents_with_query_fulltext() + { + var query = new ClrQuery + { + FullText = "hello" + }; + + var contents = await QueryAsync(_.ContentRepository, query); + + // The full text is resolved by another system, so we cannot verify the actual result. + Assert.NotNull(contents); + } + + [Fact] + public async Task Should_query_contents_with_query_filter() + { + var query = new ClrQuery + { + Filter = F.Eq("data.field1.iv", 200) + }; + + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0); + + // We have a concrete query, so we expect an actual result. + Assert.NotEmpty(contents); + } + + [Fact] + public async Task Should_query_contents_with_query_filter_and_id() + { + var query = new ClrQuery + { + Filter = F.Eq("data.value.iv", 12) + }; + + var contents = await QueryAsync(_.ContentRepository, query, 1000, 0, reference: DomainId.NewGuid()); + + // We do not insert test entities with references, so we cannot verify the actual result. + Assert.Empty(contents); + } + + [Fact] + public async Task Should_query_contents_with_random_count() + { + var query = new ClrQuery + { + Random = 40 + }; + + var contents = await QueryAsync(_.ContentRepository, query); + + // We do not insert test entities with references, so we cannot verify the actual. + Assert.Equal(40, contents.Count); + } + + private async Task> QueryAsync(IContentRepository contentRepository, + ClrQuery clrQuery, + int take = 1000, + int skip = 100, + DomainId reference = default) + { + if (clrQuery.Take == long.MaxValue) + { + clrQuery.Take = take; + } + + if (clrQuery.Skip == 0) + { + clrQuery.Skip = skip; + } + + if (clrQuery.Sort == null || clrQuery.Sort.Count == 0) + { + clrQuery.Sort = new List + { + new SortNode("LastModified", SortOrder.Descending), + new SortNode("Id", SortOrder.Ascending) + }; + } + + var q = + Q.Empty + .WithoutTotal() + .WithQuery(clrQuery) + .WithReference(reference); + + var contents = await contentRepository.QueryAsync(_.RandomApp(), _.RandomSchema(), q, SearchScope.All); + + return contents; + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs index b4e6330b5..3ae6b7e5c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs @@ -576,29 +576,61 @@ namespace Squidex.Infrastructure.Queries } [Fact] - public void Should_parse_query() + public void Should_parse_filter() { - var json = new { skip = 10, take = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + var json = new { Filter = new { path = "string", op = "eq", value = "Hello" } }; - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + AssertQuery(json, "Filter: string == 'Hello'"); } [Fact] - public void Should_parse_query_with_top() + public void Should_parse_fulltext() { - var json = new { skip = 10, top = 20, FullText = "Hello", Filter = new { path = "string", op = "eq", value = "Hello" } }; + var json = new { FullText = "Hello" }; - AssertQuery(json, "Filter: string == 'Hello'; FullText: 'Hello'; Skip: 10; Take: 20"); + AssertQuery(json, "FullText: 'Hello'"); } [Fact] - public void Should_parse_query_with_sorting() + public void Should_parse_sort() { var json = new { sort = new[] { new { path = "string", order = "ascending" } } }; AssertQuery(json, "Sort: string Ascending"); } + [Fact] + public void Should_parse_top() + { + var json = new { top = 20 }; + + AssertQuery(json, "Take: 20"); + } + + [Fact] + public void Should_parse_take() + { + var json = new { take = 20 }; + + AssertQuery(json, "Take: 20"); + } + + [Fact] + public void Should_parse_skip() + { + var json = new { skip = 10 }; + + AssertQuery(json, "Skip: 10"); + } + + [Fact] + public void Should_parse_random() + { + var json = new { random = 4 }; + + AssertQuery(json, "Random: 4"); + } + [Fact] public void Should_throw_exception_for_invalid_query() { diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs index dbee4990c..c755d3c50 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromODataTests.cs @@ -442,34 +442,34 @@ namespace Squidex.Infrastructure.Queries } [Fact] - public void Should_parse_filter_with_full_text_numbers() + public void Should_full_text() { - var i = _Q("$search=\"33k\""); - var o = _C("FullText: '33k'"); + var i = _Q("$search=Duck"); + var o = _C("FullText: 'Duck'"); Assert.Equal(o, i); } [Fact] - public void Should_parse_filter_with_full_text() + public void Should_text_and_multiple_terms() { - var i = _Q("$search=Duck"); - var o = _C("FullText: 'Duck'"); + var i = _Q("$search=Dagobert or Donald"); + var o = _C("FullText: 'Dagobert or Donald'"); Assert.Equal(o, i); } [Fact] - public void Should_parse_filter_with_full_text_and_multiple_terms() + public void Should_full_text_numbers() { - var i = _Q("$search=Dagobert or Donald"); - var o = _C("FullText: 'Dagobert or Donald'"); + var i = _Q("$search=\"33k\""); + var o = _C("FullText: '33k'"); Assert.Equal(o, i); } [Fact] - public void Should_make_orderby_with_single_field() + public void Should_parse_orderby() { var i = _Q("$orderby=age desc"); var o = _C("Sort: age Descending"); @@ -478,7 +478,7 @@ namespace Squidex.Infrastructure.Queries } [Fact] - public void Should_make_orderby_with_multiple_field() + public void Should_parse_orderby_with_multiple_field() { var i = _Q("$orderby=age, incomeMio desc"); var o = _C("Sort: age Ascending, incomeMio Descending"); @@ -487,10 +487,28 @@ namespace Squidex.Infrastructure.Queries } [Fact] - public void Should_parse_filter_and_take() + public void Should_parse_top() + { + var i = _Q("$top=3"); + var o = _C("Take: 3"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_skip() + { + var i = _Q("$skip=4"); + var o = _C("Skip: 4"); + + Assert.Equal(o, i); + } + + [Fact] + public void Should_parse_random() { - var i = _Q("$top=3&$skip=4"); - var o = _C("Skip: 4; Take: 3"); + var i = _Q("$random=4"); + var o = _C("Random: 4"); Assert.Equal(o, i); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs index 710619446..d57c9b0ff 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Tasks/PartitionedActionBlockTests.cs @@ -17,8 +17,6 @@ namespace Squidex.Infrastructure.Tasks [Fact] public async Task Should_propagate_in_order() { - var random = new Random(); - var lists = new List[Partitions]; for (var i = 0; i < Partitions; i++) @@ -28,7 +26,7 @@ namespace Squidex.Infrastructure.Tasks var block = new PartitionedActionBlock<(int P, int V)>(x => { - random.Next(10); + Random.Shared.Next(10); lists[x.P].Add(x.V); diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 073d1dd42..0e5cc8034 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -46,14 +46,15 @@ namespace TestSuite.ApiTests { var q = new ContentQuery { OrderBy = "data/number/iv asc" }; - var itemsByQ = await _.Contents.GetAsync(q); - var itemsIds = itemsByQ.Items.Take(3).Select(x => x.Id).ToHashSet(); - var itemsById = await _.Contents.GetAsync(new ContentQuery { Ids = itemsIds }); + var items_0 = await _.Contents.GetAsync(q); + var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); - Assert.Equal(3, itemsById.Items.Count); - Assert.Equal(3, itemsById.Total); + var items_1 = await _.Contents.GetAsync(new ContentQuery { Ids = itemsIds }); - foreach (var item in itemsById.Items) + Assert.Equal(3, items_1.Items.Count); + Assert.Equal(3, items_1.Total); + + foreach (var item in items_1.Items) { Assert.Equal(_.AppName, item.AppName); Assert.Equal(_.SchemaName, item.SchemaName); @@ -65,14 +66,15 @@ namespace TestSuite.ApiTests { var q = new ContentQuery { OrderBy = "data/number/iv asc" }; - var itemsByQ = await _.Contents.GetAsync(q); - var itemsIds = itemsByQ.Items.Take(3).Select(x => x.Id).ToHashSet(); - var itemsById = await _.SharedContents.GetAsync(itemsIds); + var items_0 = await _.Contents.GetAsync(q); + var itemsIds = items_0.Items.Take(3).Select(x => x.Id).ToHashSet(); + + var items_1 = await _.SharedContents.GetAsync(itemsIds); - Assert.Equal(3, itemsById.Items.Count); - Assert.Equal(3, itemsById.Total); + Assert.Equal(3, items_1.Items.Count); + Assert.Equal(3, items_1.Total); - foreach (var item in itemsById.Items) + foreach (var item in items_1.Items) { Assert.Equal(_.AppName, item.AppName); Assert.Equal(_.SchemaName, item.SchemaName); @@ -80,7 +82,43 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_all_with_odata() + public async Task Should_query_by_ids_filter() + { + var q0 = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; + + var items_0 = await _.Contents.GetAsync(q0); + + var q1 = new ContentQuery + { + JsonQuery = new + { + sort = new[] + { + new + { + path = "data.number.iv" + } + }, + filter = new + { + or = items_0.Items.Select(x => new + { + path = "id", + op = "eq", + value = x.Id + }).ToArray() + } + } + }; + + var items_1 = await _.Contents.GetAsync(q1); + + AssertItems(items_0, 3, new[] { 4, 5, 6 }); + AssertItems(items_1, 3, new[] { 4, 5, 6 }); + } + + [Fact] + public async Task Should_query_all_with_odata() { var q = new ContentQuery { OrderBy = "data/number/iv asc" }; @@ -90,7 +128,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_all_with_json() + public async Task Should_query_all_with_json() { var q = new ContentQuery { @@ -112,7 +150,33 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_skip_with_odata() + public async Task Should_query_random_with_odata() + { + var q = new ContentQuery { Random = 5 }; + + var items = await _.Contents.GetAsync(q); + + Assert.Equal(5, items.Items.Count); + } + + [Fact] + public async Task Should_query_random_with_json() + { + var q = new ContentQuery + { + JsonQuery = new + { + random = 5 + } + }; + + var items = await _.Contents.GetAsync(q); + + Assert.Equal(5, items.Items.Count); + } + + [Fact] + public async Task Should_query_by_skip_with_odata() { var q = new ContentQuery { OrderBy = "data/number/iv asc", Skip = 5 }; @@ -122,7 +186,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_skip_with_json() + public async Task Should_query_by_skip_with_json() { var q = new ContentQuery { @@ -145,7 +209,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_skip_and_top_with_odata() + public async Task Should_query_by_skip_and_top_with_odata() { var q = new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }; @@ -155,7 +219,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_skip_and_top_with_json() + public async Task Should_query_by_skip_and_top_with_json() { var q = new ContentQuery { @@ -179,7 +243,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_filter_with_odata() + public async Task Should_query_by_filter_with_odata() { var q = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; @@ -189,7 +253,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_filter_with_json() + public async Task Should_query_by_filter_with_json() { var q = new ContentQuery { @@ -229,7 +293,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_json_filter_with_json() + public async Task Should_query_by_json_filter_with_json() { var q = new ContentQuery { @@ -269,7 +333,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_full_text_with_odata() + public async Task Should_query_by_full_text_with_odata() { var q = new ContentQuery { Search = "2" }; @@ -279,7 +343,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_full_text_with_json() + public async Task Should_query_by_full_text_with_json() { var q = new ContentQuery { @@ -295,7 +359,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_near_location_with_odata() + 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" }; @@ -305,7 +369,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_near_location_with_json() + public async Task Should_query_by_near_location_with_json() { var q = new ContentQuery { @@ -331,7 +395,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_near_geoson_location_with_odata() + 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" }; @@ -390,7 +454,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_items_by_near_geoson_location_with_json() + public async Task Should_query_by_near_geoson_location_with_json() { var q = new ContentQuery { @@ -444,42 +508,6 @@ namespace TestSuite.ApiTests Assert.Equal(555, value); } - [Fact] - public async Task Should_return_items_by_ids() - { - var q0 = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; - - var items_0 = await _.Contents.GetAsync(q0); - - var q1 = new ContentQuery - { - JsonQuery = new - { - sort = new[] - { - new - { - path = "data.number.iv" - } - }, - filter = new - { - or = items_0.Items.Select(x => new - { - path = "id", - op = "eq", - value = x.Id - }).ToArray() - } - } - }; - - var items_1 = await _.Contents.GetAsync(q1); - - AssertItems(items_0, 3, new[] { 4, 5, 6 }); - AssertItems(items_1, 3, new[] { 4, 5, 6 }); - } - [Fact] public async Task Should_create_and_query_with_variable_graphql() { @@ -516,7 +544,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_batch_query_items_with_graphql() + public async Task Should_query_with_graphql_batching() { var query1 = new { @@ -566,7 +594,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_query_items_with_graphql() + public async Task Should_query_with_graphql() { var query = new { @@ -594,7 +622,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_query_items_with_graphql_get() + public async Task Should_query_with_graphql_get() { var query = new { @@ -622,7 +650,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_query_items_with_graphql_with_dynamic() + public async Task Should_query_with_graphql_with_dynamic() { var query = new { @@ -646,7 +674,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_query_items_complex_search() + public async Task Should_query_with_grapqhl_complex_search() { var query = new { @@ -671,7 +699,7 @@ namespace TestSuite.ApiTests } [Fact] - public async Task Should_return_correct_content_type_for_graphql() + public async Task Should_query_correct_content_type_for_graphql() { var query = new { @@ -694,13 +722,10 @@ namespace TestSuite.ApiTests using (var client = _.ClientManager.CreateHttpClient()) { + // Create the request manually to check the content type. var response = await client.PostAsync(_.ClientManager.GenerateUrl($"api/content/{_.AppName}/graphql/batch"), query.ToContent()); Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType); - - var result = await response.Content.ReadAsJsonAsync>(); - - Assert.Equal(result.Data.Items.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); } } diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index ba0b66893..e3a87f103 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -16,8 +16,8 @@ - - + +