From 3b85fedd2b48a74b4026de0f11681a5faa09cacf Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 10 Jul 2017 21:24:44 +0200 Subject: [PATCH] Several fixes --- .../Contents/GraphQL/GraphQLModel.cs | 116 ++-- .../Contents/GraphQL/QueryContext.cs | 4 +- .../GraphQL/Types/ContentDataGraphType.cs | 16 +- .../GraphQL/Types/ContentGraphType.cs | 17 +- ...tQueryType.cs => ContentQueryGraphType.cs} | 12 +- .../Contents/GraphQL/Types/NoopGraphType.cs | 37 ++ .../pages/schema/schema-page.component.ts | 2 +- .../Contents/GraphQLTests.cs | 607 ++++++++++++++++++ .../Contents/TestData/FakeAssetEntity.cs | 48 ++ .../Contents/TestData/FakeContentEntity.cs | 36 ++ 10 files changed, 814 insertions(+), 81 deletions(-) rename src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/{ContentQueryType.cs => ContentQueryGraphType.cs} (94%) create mode 100644 src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs create mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs create mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs create mode 100644 tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs index 1b86cfc15..3a90b6144 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL public sealed class GraphQLModel : IGraphQLContext { private readonly Dictionary> fieldInfos; - private readonly Dictionary schemaTypes = new Dictionary(); + private readonly Dictionary schemaTypes = new Dictionary(); private readonly Dictionary schemas; private readonly PartitionResolver partitionResolver; private readonly IGraphType assetType = new AssetGraphType(); @@ -45,97 +45,93 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL IGraphType assetListType = new ListGraphType(new NonNullGraphType(assetType)); - var stringInfos = - (new StringGraphType(), defaultResolver); - - var booleanInfos = - (new BooleanGraphType(), defaultResolver); - - var numberInfos = - (new FloatGraphType(), defaultResolver); - - var dateTimeInfos = - (new DateGraphType(), defaultResolver); - - var jsonInfos = - (new JsonScalarType(), defaultResolver); - - var geolocationInfos = - (new GeolocationScalarType(), defaultResolver); - fieldInfos = new Dictionary> { { typeof(StringField), - field => stringInfos + field => ResolveDefault("String") }, { typeof(BooleanField), - field => booleanInfos + field => ResolveDefault("Boolean") }, { typeof(NumberField), - field => numberInfos + field => ResolveDefault("Float") }, { typeof(DateTimeField), - field => dateTimeInfos + field => ResolveDefault("Date") }, { typeof(JsonField), - field => jsonInfos + field => ResolveDefault("Json") }, { typeof(GeolocationField), - field => geolocationInfos + field => ResolveDefault("Geolocation") }, { typeof(AssetsField), - field => - { - var resolver = new FuncFieldResolver(c => - { - var context = (QueryContext)c.UserContext; - var contentIds = c.Source.GetOrDefault(c.FieldName); - - return context.GetReferencedAssets(contentIds); - }); - - return (assetListType, resolver); - } + field => { return ResolveAssets(assetListType); } }, { typeof(ReferencesField), - field => - { - var schemaId = ((ReferencesField)field).Properties.SchemaId; - var schemaType = GetSchemaType(schemaId); + field => { return ResolveReferences(field); } + } + }; - if (schemaType == null) - { - return (null, null); - } + this.schemas = schemas.ToDictionary(x => x.Id); + + graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; - var resolver = new FuncFieldResolver(c => - { - var context = (QueryContext)c.UserContext; - var contentIds = c.Source.GetOrDefault(c.FieldName); + foreach (var schemaType in schemaTypes.Values) + { + schemaType.Initialize(); + } + } - return context.GetReferencedContents(schemaId, contentIds); - }); + private (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name) + { + return (new NoopGraphType(name), new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName))); + } - var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); + private static ValueTuple ResolveAssets(IGraphType assetListType) + { + var resolver = new FuncFieldResolver(c => + { + var context = (QueryContext)c.UserContext; + var contentIds = c.Source.GetOrDefault(c.FieldName); - return (schemaFieldType, resolver); - } - } - }; + return context.GetReferencedAssets(contentIds); + }); - this.schemas = schemas.ToDictionary(x => x.Id); - - graphQLSchema = new GraphQLSchema { Query = new ContentQueryType(this, this.schemas.Values) }; + return (assetListType, resolver); } - + + private ValueTuple ResolveReferences(Field field) + { + var schemaId = ((ReferencesField)field).Properties.SchemaId; + var schemaType = GetSchemaType(schemaId); + + if (schemaType == null) + { + return (null, null); + } + + var resolver = new FuncFieldResolver(c => + { + var context = (QueryContext)c.UserContext; + var contentIds = c.Source.GetOrDefault(c.FieldName); + + return context.GetReferencedContents(schemaId, contentIds); + }); + + var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); + + return (schemaFieldType, resolver); + } + public async Task ExecuteAsync(QueryContext context, GraphQLQuery query) { Guard.NotNull(context, nameof(context)); diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs index 492113b99..a49b83e2a 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/QueryContext.cs @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { Task.Run(async () => { - var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, string.Empty, int.MaxValue).ConfigureAwait(false); + var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue).ConfigureAwait(false); foreach (var asset in assets) { @@ -146,7 +146,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL { Task.Run(async () => { - var contents = await contentRepository.QueryAsync(app, schemaId, false, notLoadedContents, string.Empty).ConfigureAwait(false); + var contents = await contentRepository.QueryAsync(app, schemaId, false, notLoadedContents, null).ConfigureAwait(false); foreach (var content in contents) { diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs index 0295db2de..cea509cd1 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -19,10 +19,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types { public sealed class ContentDataGraphType : ObjectGraphType { - private static readonly IFieldResolver FieldResolver = - new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName)); - - public ContentDataGraphType(Schema schema, IGraphQLContext graphQLContext) + public ContentDataGraphType(Schema schema, IGraphQLContext context) { var schemaName = schema.Properties.Label.WithFallback(schema.Name); @@ -30,7 +27,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types foreach (var field in schema.Fields.Where(x => !x.IsHidden)) { - var fieldInfo = graphQLContext.GetGraphType(field); + var fieldInfo = context.GetGraphType(field); if (fieldInfo.ResolveType != null) { @@ -40,7 +37,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" }; - var partition = graphQLContext.ResolvePartition(field.Paritioning); + var partition = context.ResolvePartition(field.Paritioning); foreach (var partitionItem in partition) { @@ -55,10 +52,13 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; + var fieldResolver = + new FuncFieldResolver(c => c.Source.GetOrDefault(field.Name)); + AddField(new FieldType { - Name = field.Name, - Resolver = FieldResolver, + Name = field.Name.ToCamelCase(), + Resolver = fieldResolver, ResolvedType = fieldGraphType }); } diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs index 838fc1ef7..fa6d4a674 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentGraphType.cs @@ -16,11 +16,20 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types { public sealed class ContentGraphType : ObjectGraphType { - public ContentGraphType(Schema schema, IGraphQLContext graphQLContext) + private readonly Schema schema; + private readonly IGraphQLContext context; + + public ContentGraphType(Schema schema, IGraphQLContext context) { - var schemaName = schema.Properties.Label.WithFallback(schema.Name); + this.schema = schema; + this.context = context; Name = $"{schema.Name.ToPascalCase()}Dto"; + } + + public void Initialize() + { + var schemaName = schema.Properties.Label.WithFallback(schema.Name); AddField(new FieldType { @@ -69,12 +78,12 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types ResolvedType = new NonNullGraphType(new StringGraphType()), Description = $"The user that has updated the {schemaName} content last." }); - + AddField(new FieldType { Name = "data", Resolver = Resolver(x => x.Data), - ResolvedType = new NonNullGraphType(new ContentDataGraphType(schema, graphQLContext)), + ResolvedType = new NonNullGraphType(new ContentDataGraphType(schema, context)), Description = $"The data of the {schemaName} content." }); diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs similarity index 94% rename from src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs rename to src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs index 173cb12ea..7365f0427 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryType.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/ContentQueryGraphType.cs @@ -16,9 +16,9 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types { - public sealed class ContentQueryType : ObjectGraphType + public sealed class ContentQueryGraphType : ObjectGraphType { - public ContentQueryType(IGraphQLContext graphQLContext, IEnumerable schemaEntities) + public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable schemaEntities) { AddAssetFind(graphQLContext); AddAssetsQuery(graphQLContext); @@ -26,9 +26,9 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types foreach (var schemaEntity in schemaEntities) { var schemaName = schemaEntity.Schema.Properties.Label.WithFallback(schemaEntity.Schema.Name); - var schemaType = new ContentGraphType(schemaEntity.Schema, graphQLContext); + var schemaType = graphQLContext.GetSchemaType(schemaEntity.Id); - AddContentFind(schemaEntity, schemaName, schemaType); + AddContentFind(schemaEntity, schemaType, schemaName); AddContentQuery(schemaEntity, schemaType, schemaName); } @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types }); } - private void AddContentFind(ISchemaEntity schemaEntity, string schemaName, IGraphType schemaType) + private void AddContentFind(ISchemaEntity schemaEntity, IGraphType schemaType, string schemaName) { AddField(new FieldType { @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types var argTop = c.GetArgument("top", 20); var argSkip = c.GetArgument("skip", 0); - var argQuery = c.GetArgument("query", string.Empty); + var argQuery = c.GetArgument("search", string.Empty); return context.QueryAssetsAsync(argQuery, argSkip, argTop); }), diff --git a/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs new file mode 100644 index 000000000..36218f6eb --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Contents/GraphQL/Types/NoopGraphType.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// NoopGraphType.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using GraphQL.Language.AST; +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Read.Contents.GraphQL.Types +{ + public sealed class NoopGraphType : ScalarGraphType + { + public NoopGraphType(string name) + { + Name = name; + } + + public override object Serialize(object value) + { + return value; + } + + public override object ParseValue(object value) + { + return value; + } + + public override object ParseLiteral(IValue value) + { + throw new NotSupportedException(); + } + } +} diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 9237aafa7..291c33279 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -71,7 +71,7 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { name: ['', [ Validators.maxLength(40), - ValidatorsEx.pattern('[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*', 'Name must be a valid javascript name in camel case.') + ValidatorsEx.pattern('[a-z0-9]+(\\-[a-zA-Z0-9]+)*', 'Name must be a valid javascript name in camel case.') ]], isLocalizable: [false] }); diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs new file mode 100644 index 000000000..3b776b689 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs @@ -0,0 +1,607 @@ +// ========================================================================== +// GraphQLTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Read.Contents.GraphQL; +using Squidex.Domain.Apps.Read.Contents.Repositories; +using Squidex.Domain.Apps.Read.Contents.TestData; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Repositories; +using Xunit; +using NodaTime.Extensions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Read.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.Contents +{ + public class GraphQLTests + { + private static readonly Guid schemaId = Guid.NewGuid(); + private static readonly Guid appId = Guid.NewGuid(); + + private readonly Schema schema = + Schema.Create("my-schema", new SchemaProperties()) + .AddOrUpdateField(new JsonField(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties())) + .AddOrUpdateField(new StringField(2, "my-string", Partitioning.Language, + new StringFieldProperties())) + .AddOrUpdateField(new NumberField(3, "my-number", Partitioning.Invariant, + new NumberFieldProperties())) + .AddOrUpdateField(new AssetsField(4, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties())) + .AddOrUpdateField(new BooleanField(5, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties())) + .AddOrUpdateField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties())) + .AddOrUpdateField(new ReferencesField(7, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId })) + .AddOrUpdateField(new GeolocationField(8, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties())); + + private readonly Mock schemaRepository = new Mock(); + private readonly Mock contentRepository = new Mock(); + private readonly Mock assetRepository = new Mock(); + private readonly IAppEntity app; + private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); + private readonly IGraphQLInvoker sut; + + public GraphQLTests() + { + var appEntity = new Mock(); + appEntity.Setup(x => x.Id).Returns(appId); + appEntity.Setup(x => x.PartitionResolver).Returns(x => InvariantPartitioning.Instance); + + app = appEntity.Object; + + var schemaEntity = new Mock(); + schemaEntity.Setup(x => x.Id).Returns(schemaId); + schemaEntity.Setup(x => x.Name).Returns(schema.Name); + schemaEntity.Setup(x => x.Schema).Returns(schema); + schemaEntity.Setup(x => x.IsPublished).Returns(true); + + var schemas = new List { schemaEntity.Object }; + + schemaRepository.Setup(x => x.QueryAllAsync(appId)).Returns(Task.FromResult>(schemas)); + + sut = new CachingGraphQLInvoker(cache, schemaRepository.Object, assetRepository.Object, contentRepository.Object); + } + + [Fact] + public async Task Should_make_assets_request() + { + const string query = @" + query { + queryAssets(search: ""my-query"", top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + mimeType + fileName + fileSize + fileVersion + isImage + pixelWidth, + pixelHeight + } + }"; + + var assetEntity = CreateAsset(Guid.NewGuid()); + + var assets = new List { assetEntity }; + + assetRepository.Setup(x => x.QueryAsync(app.Id, null, null, "my-query", 30, 5)) + .Returns(Task.FromResult>(assets)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryAssets = new dynamic[] + { + new + { + id = assetEntity.Id, + version = 1, + created = assetEntity.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = assetEntity.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + mimeType = "image/png", + fileName = "MyFile.png", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600 + } + } + } + }; + + AssertJson(expected, result); + + assetRepository.VerifyAll(); + } + + [Fact] + public async Task Should_make_asset_request() + { + var assetId = Guid.NewGuid(); + var assetEntity = CreateAsset(Guid.NewGuid()); + + var query = $@" + query {{ + findAsset(id: ""{assetId}"") {{ + id + version + created + createdBy + lastModified + lastModifiedBy + mimeType + fileName + fileSize + fileVersion + isImage + pixelWidth, + pixelHeight + }} + }}"; + + assetRepository.Setup(x => x.FindAssetAsync(assetId)) + .Returns(Task.FromResult(assetEntity)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = new + { + id = assetEntity.Id, + version = 1, + created = assetEntity.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = assetEntity.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + mimeType = "image/png", + fileName = "MyFile.png", + fileSize = 1024, + fileVersion = 123, + isImage = true, + pixelWidth = 800, + pixelHeight = 600 + } + } + }; + + AssertJson(expected, result); + + assetRepository.VerifyAll(); + } + + [Fact] + public async Task Should_make_contents_request() + { + const string query = @" + query { + queryMySchemaContents(top: 30, skip: 5) { + id + version + created + createdBy + lastModified + lastModifiedBy + data { + myString { + iv + } + myNumber { + iv + } + myBoolean { + iv + } + myDatetime { + iv + } + myJson { + iv + } + myGeolocation { + iv + } + } + } + }"; + + var contentEntity = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); + + var contents = new List { contentEntity }; + + contentRepository.Setup(x => x.QueryAsync(app, schemaId, false, null, "?$top=30&$skip=5")) + .Returns(Task.FromResult>(contents)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + queryMySchemaContents = new dynamic[] + { + new + { + id = contentEntity.Id, + version = 1, + created = contentEntity.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = contentEntity.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + data = new + { + myString = new + { + iv = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = contentEntity.LastModified.ToDateTimeUtc() + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + } + } + } + } + } + }; + + AssertJson(expected, result); + + contentRepository.VerifyAll(); + } + + [Fact] + public async Task Should_make_content_request() + { + var contentId = Guid.NewGuid(); + var contentEntity = CreateContent(contentId, Guid.Empty, Guid.Empty); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + version + created + createdBy + lastModified + lastModifiedBy + data {{ + myString {{ + iv + }} + myNumber {{ + iv + }} + myBoolean {{ + iv + }} + myDatetime {{ + iv + }} + myJson {{ + iv + }} + myGeolocation {{ + iv + }} + }} + }} + }}"; + + contentRepository.Setup(x => x.FindContentAsync(app, schemaId, contentId)) + .Returns(Task.FromResult(contentEntity)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = contentEntity.Id, + version = 1, + created = contentEntity.Created.ToDateTimeUtc(), + createdBy = "subject:user1", + lastModified = contentEntity.LastModified.ToDateTimeUtc(), + lastModifiedBy = "subject:user2", + data = new + { + myString = new + { + iv = "value" + }, + myNumber = new + { + iv = 1 + }, + myBoolean = new + { + iv = true + }, + myDatetime = new + { + iv = contentEntity.LastModified.ToDateTimeUtc() + }, + myJson = new + { + iv = new + { + value = 1 + } + }, + myGeolocation = new + { + iv = new + { + latitude = 10, + longitude = 20 + } + } + } + } + } + }; + + AssertJson(expected, result); + + contentRepository.VerifyAll(); + } + + [Fact] + public async Task Should_make_content_request_and_resolve_references() + { + var contentRefId = Guid.NewGuid(); + var contentRefEntity = CreateContent(contentRefId, Guid.Empty, Guid.Empty); + + var contentId = Guid.NewGuid(); + var contentEntity = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + data {{ + myReferences {{ + iv {{ + id + }} + }} + }} + }} + }}"; + + var refContents = new List { contentRefEntity }; + + contentRepository.Setup(x => x.FindContentAsync(app, schemaId, contentId)) + .Returns(Task.FromResult(contentEntity)) + .Verifiable(); + + contentRepository.Setup(x => x.QueryAsync(app, schemaId, false, new HashSet {contentRefId }, null)) + .Returns(Task.FromResult>(refContents)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = contentEntity.Id, + data = new + { + myReferences = new + { + iv = new[] + { + new + { + id = contentRefId + } + } + } + } + } + } + }; + + AssertJson(expected, result); + + contentRepository.VerifyAll(); + } + + [Fact] + public async Task Should_make_content_request_and_resolve_assets() + { + var assetRefId = Guid.NewGuid(); + var assetRefEntity = CreateAsset(assetRefId); + + var contentId = Guid.NewGuid(); + var contentEntity = CreateContent(contentId, Guid.Empty, assetRefId); + + var query = $@" + query {{ + findMySchemaContent(id: ""{contentId}"") {{ + id + data {{ + myAssets {{ + iv {{ + id + }} + }} + }} + }} + }}"; + + var refAssets = new List { assetRefEntity }; + + contentRepository.Setup(x => x.FindContentAsync(app, schemaId, contentId)) + .Returns(Task.FromResult(contentEntity)) + .Verifiable(); + + assetRepository.Setup(x => x.QueryAsync(app.Id, null, new HashSet { assetRefId }, null, int.MaxValue, 0)) + .Returns(Task.FromResult>(refAssets)) + .Verifiable(); + + dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = contentEntity.Id, + data = new + { + myAssets = new + { + iv = new[] + { + new + { + id = assetRefId + } + } + } + } + } + } + }; + + AssertJson(expected, result); + + contentRepository.VerifyAll(); + } + + private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId) + { + var now = DateTime.UtcNow.ToInstant(); + + var data = + new NamedContentData() + .AddField("my-json", + new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))) + .AddField("my-string", + new ContentFieldData().AddValue("iv", "value")) + .AddField("my-assets", + new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) + .AddField("my-number", + new ContentFieldData().AddValue("iv", 1)) + .AddField("my-boolean", + new ContentFieldData().AddValue("iv", true)) + .AddField("my-datetime", + new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) + .AddField("my-references", + new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) + .AddField("my-geolocation", + new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); + + var contentEntity = new FakeContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken("subject", "user1"), + LastModified = now, + LastModifiedBy = new RefToken("subject", "user2"), + Data = data + }; + + return contentEntity; + } + + private static IAssetEntity CreateAsset(Guid id) + { + var now = DateTime.UtcNow.ToInstant(); + + var assetEntity = new FakeAssetEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken("subject", "user1"), + LastModified = now, + LastModifiedBy = new RefToken("subject", "user2"), + FileName = "MyFile.png", + FileSize = 1024, + FileVersion = 123, + MimeType = "image/png", + IsImage = true, + PixelWidth = 800, + PixelHeight = 600 + }; + + return assetEntity; + } + + private static void AssertJson(object expected, object result) + { + var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented); + var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented); + + Assert.Equal(expectJson, resultJson); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs new file mode 100644 index 000000000..5c70ddbfc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeAssetEntity.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// MockupAssetEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Read.Assets; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.Contents.TestData +{ + public sealed class FakeAssetEntity : IAssetEntity + { + public Guid Id { get; set; } + + public Guid AppId { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public long Version { get; set; } + + public string MimeType { get; set; } + + public string FileName { get; set; } + + public long FileSize { get; set; } + + public long FileVersion { get; set; } + + public bool IsImage { get; set; } + + public bool IsDeleted { get; set; } + + public int? PixelWidth { get; set; } + + public int? PixelHeight { get; set; } + } +} diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs new file mode 100644 index 000000000..49a32afb2 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/TestData/FakeContentEntity.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// FakeContentEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.Contents.TestData +{ + public sealed class FakeContentEntity : IContentEntity + { + public Guid Id { get; set; } + + public Guid AppId { get; set; } + + public Instant Created { get; set; } + + public Instant LastModified { get; set; } + + public RefToken CreatedBy { get; set; } + + public RefToken LastModifiedBy { get; set; } + + public long Version { get; set; } + + public bool IsPublished { get; set; } + + public NamedContentData Data { get; set; } + } +}