// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Globalization; using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Queries; using Squidex.Infrastructure.States; #pragma warning disable xUnit1044 // Avoid using TheoryData type arguments that are not serializable #pragma warning disable MA0040 // Forward the CancellationToken parameter to methods that take one namespace Squidex.Shared; public abstract class AssetRepositoryTests : GivenContext { private const int NumValues = 250; private static readonly DomainId ParentId = DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d451"); private static readonly NamedId[] AppIds = [ NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d452"), "my-app1"), NamedId.Of(DomainId.Create("3b5ba909-e5a5-4858-9d0d-df4ff922d453"), "my-app2"), ]; private readonly string randomValue = Random.Shared.Next(NumValues).ToString(CultureInfo.InvariantCulture); private readonly DomainId appId; protected AssetRepositoryTests() { appId = AppIds[Random.Shared.Next(AppIds.Length)].Id; } protected abstract Task CreateSutAsync(); protected async Task CreateAndPrepareSutAsync() { var sut = await CreateSutAsync(); if (sut is not ISnapshotStore store) { return sut; } if (await sut.StreamAll(AppIds[0].Id).AnyAsync()) { return sut; } var batch = new List>(); async Task ExecuteBatchAsync(Asset? entity) { if (entity != null) { batch.Add(new SnapshotWriteJob(entity.UniqueId, entity, 0)); } if ((entity == null || batch.Count >= 1000) && batch.Count > 0) { await store.WriteManyAsync(batch); batch.Clear(); } } foreach (var forAppId in AppIds) { for (var i = 0; i < NumValues; i++) { var fileName = i.ToString(CultureInfo.InvariantCulture); for (var j = 0; j < NumValues; j++) { var asset = CreateAsset() with { AppId = forAppId, FileHash = fileName, FileName = fileName, Metadata = new AssetMetadata { ["value"] = JsonValue.Create($"value_{j}"), }, Tags = [ $"tag_{j}", ], Slug = fileName, }; await ExecuteBatchAsync(asset); } } var byParent = CreateAsset() with { AppId = forAppId, ParentId = ParentId, FileHash = "0", FileName = "0", Metadata = new AssetMetadata { ["value"] = JsonValue.Create("0"), }, Tags = [ "tag_0", ], Slug = "0", }; await ExecuteBatchAsync(byParent); } await ExecuteBatchAsync(null); return sut; } public static readonly TheoryData ParentIds = new TheoryData { { null }, { DomainId.Empty }, }; [Fact] public async Task Should_find_asset_by_id_only() { var sut = await CreateAndPrepareSutAsync(); var asset1 = await sut.StreamAll(appId).FirstAsync(); var asset2 = await sut.FindAssetAsync(asset1.Id); // The Slug is random here, as it does not really matter. Assert.NotNull(asset2); } [Fact] public async Task Should_find_asset_by_id() { var sut = await CreateAndPrepareSutAsync(); var asset1 = await sut.StreamAll(appId).FirstAsync(); var asset2 = await sut.FindAssetAsync(appId, asset1.Id, false); // The ID is random here, as it does not really matter. Assert.NotNull(asset2); } [Fact] public async Task Should_find_asset_by_hash() { var sut = await CreateAndPrepareSutAsync(); var asset = await sut.FindAssetByHashAsync(appId, randomValue, randomValue, 1024); // The Hash is random here, as it does not really matter. Assert.NotNull(asset); } [Fact] public async Task Should_find_asset_by_slug() { var sut = await CreateAndPrepareSutAsync(); var asset = await sut.FindAssetBySlugAsync(appId, randomValue, false); // The Slug is random here, as it does not really matter. Assert.NotNull(asset); } [Fact] public async Task Should_query_ids() { var sut = await CreateAndPrepareSutAsync(); var assetIds = await sut.StreamAll(appId).Take(100).Select(x => x.Id).ToHashSetAsync(); var assets = await sut.QueryIdsAsync(appId, assetIds); // The IDs are valid. Assert.Equal(assetIds.Count, assets.Count); } [Fact] public async Task Should_query_child_ids() { var sut = await CreateAndPrepareSutAsync(); var assets = await sut.QueryChildIdsAsync(appId, ParentId); // No pagination is going on here. Assert.Single(assets); } [Theory] [MemberData(nameof(ParentIds))] public async Task Should_query_assets(DomainId? parentId) { var query = new ClrQuery(); var assets = await QueryAsync(parentId, query); // 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] [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_tags(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.Eq("tags", $"tag_{randomValue}"), }; 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_in_query(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.In("tags", new List { randomValue, "other" }), }; 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_fileName(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.And( ClrFilter.Eq("tags", $"tag_{randomValue}"), ClrFilter.Contains("fileName", randomValue)), }; var assets = await QueryAsync(parentId, query); // The filter is a random value from the expected result set. Assert.NotEmpty(assets); } [Theory] [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_fileName(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.Contains("fileName", randomValue), }; var assets = await QueryAsync(parentId, query); // The filter is a random value from the expected result set. Assert.NotEmpty(assets); } [Theory] [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_metadata(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.Contains("metadata.value", $"value_{randomValue}"), }; var assets = await QueryAsync(parentId, query); // The filter is a random value from the expected result set. Assert.NotEmpty(assets); } [Theory] [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_fileName_and_tags(DomainId? parentId) { var query = new ClrQuery { Filter = ClrFilter.And( ClrFilter.Contains("fileName", randomValue), ClrFilter.Eq("tags", $"tag_{randomValue}")), }; var assets = await QueryAsync(parentId, query); // The filter is a random value from the expected result set. Assert.NotEmpty(assets); } [Theory] [MemberData(nameof(ParentIds))] public async Task Should_query_assets_by_ids(DomainId? parentId) { var sut = await CreateAndPrepareSutAsync(); var q = Q.Empty .WithIds(await sut.StreamAll(appId).Take(100).Select(x => x.Id).ToHashSetAsync()); var assets = await sut.QueryAsync(appId, parentId, q); // The filter is a random value from the expected result set. Assert.NotEmpty(assets); } private async Task> QueryAsync(DomainId? parentId, ClrQuery clrQuery, int top = 1000, int skip = 0) { var sut = await CreateAndPrepareSutAsync(); clrQuery.Top = top; clrQuery.Skip = skip; clrQuery.Sort ??= []; if (clrQuery.Sort.Count == 0) { clrQuery.Sort.Add(new SortNode("lastModified", SortOrder.Descending)); } if (!clrQuery.Sort.Exists(x => x.Path.Equals("id"))) { clrQuery.Sort.Add(new SortNode("id", SortOrder.Ascending)); } var q = Q.Empty .WithoutTotal() .WithQuery(clrQuery); var assets = await sut.QueryAsync(appId, parentId, q); return assets; } }