diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 168220057..778023851 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -6,8 +6,8 @@ // ========================================================================== using System; -using System.Linq; using System.Threading.Tasks; +using GraphQL; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NodaTime; @@ -19,6 +19,7 @@ using Squidex.Infrastructure; using Squidex.Log; #pragma warning disable SA1313 // Parameter names should begin with lower-case letter +#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { @@ -30,7 +31,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private readonly IServiceProvider serviceProvider; private readonly GraphQLOptions options; - public sealed record CacheEntry(GraphQLModel Model, string Hash, Instant Created); + private sealed record CacheEntry(GraphQLModel Model, string Hash, Instant Created); + + public IServiceProvider Services + { + get { return serviceProvider; } + } public CachingGraphQLService(IBackgroundCache cache, ISchemasHash schemasHash, IServiceProvider serviceProvider, IOptions options) { @@ -45,55 +51,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL this.options = options.Value; } - public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries) - { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(queries, nameof(queries)); - - var model = await GetModelAsync(context.App); - - var executionContext = - serviceProvider.GetRequiredService() - .WithContext(context); - - var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, executionContext, q))); - - return (result.Any(x => x.HasError), result.Map(x => x.Response)); - } - - public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query) + public async Task ExecuteAsync(ExecutionOptions options) { - Guard.NotNull(context, nameof(context)); - Guard.NotNull(query, nameof(query)); + var context = ((GraphQLExecutionContext)options.UserContext).Context; var model = await GetModelAsync(context.App); - var executionContext = - serviceProvider.GetRequiredService() - .WithContext(context); - - var result = await QueryInternalAsync(model, executionContext, query); - - return result; - } - - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext context, GraphQLQuery query) - { - if (string.IsNullOrWhiteSpace(query.Query)) - { - return (false, new { data = new object() }); - } - - var (data, errors) = await model.ExecuteAsync(context, query); - - if (errors?.Any() == true) - { - return (false, new { data, errors }); - } - else - { - return (false, new { data }); - } + return await model.ExecuteAsync(options); } private async Task GetModelAsync(IAppEntity app) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs new file mode 100644 index 000000000..90a5d5130 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using Microsoft.AspNetCore.WebUtilities; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public sealed class DefaultDocumentWriter : IDocumentWriter + { + private readonly IJsonSerializer jsonSerializer; + + public DefaultDocumentWriter(IJsonSerializer jsonSerializer) + { + Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + + this.jsonSerializer = jsonSerializer; + } + + public async Task WriteAsync(Stream stream, T value, CancellationToken cancellationToken = default) + { + await using (var buffer = new FileBufferingWriteStream()) + { + jsonSerializer.Serialize(value, buffer, true); + + await buffer.DrainBufferAsync(stream, cancellationToken); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 99d99ca2e..5dfc57be5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using GraphQL; using GraphQL.DataLoader; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Entities.Assets; @@ -24,55 +23,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { private static readonly List EmptyAssets = new List(); private static readonly List EmptyContents = new List(); - private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; - private readonly DataLoaderDocumentListener dataLoaderDocumentListener; - private readonly IUrlGenerator urlGenerator; - private readonly ISemanticLog log; - private readonly ICommandBus commandBus; - private Context context; - - public IUrlGenerator UrlGenerator - { - get { return urlGenerator; } - } + private readonly IDataLoaderContextAccessor dataLoaders; - public ICommandBus CommandBus - { - get { return commandBus; } - } + public IUrlGenerator UrlGenerator { get; } - public ISemanticLog Log - { - get { return log; } - } + public ICommandBus CommandBus { get; } - public override Context Context - { - get { return context; } - } + public ISemanticLog Log { get; } + + public override Context Context { get; } public GraphQLExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery, - IDataLoaderContextAccessor dataLoaderContextAccessor, DataLoaderDocumentListener dataLoaderDocumentListener, ICommandBus commandBus, IUrlGenerator urlGenerator, ISemanticLog log) + Context context, + IDataLoaderContextAccessor dataLoaders, ICommandBus commandBus, IUrlGenerator urlGenerator, ISemanticLog log) : base(assetQuery, contentQuery) { - this.commandBus = commandBus; - this.dataLoaderContextAccessor = dataLoaderContextAccessor; - this.dataLoaderDocumentListener = dataLoaderDocumentListener; - this.urlGenerator = urlGenerator; - this.log = log; - } + this.dataLoaders = dataLoaders; - public GraphQLExecutionContext WithContext(Context newContext) - { - context = newContext.Clone(b => b.WithoutCleanup().WithoutContentEnrichment()); + CommandBus = commandBus; - return this; - } + UrlGenerator = urlGenerator; - public void Setup(ExecutionOptions execution) - { - execution.Listeners.Add(dataLoaderDocumentListener); - execution.UserContext = this; + Context = context.Clone(b => b + .WithoutCleanup() + .WithoutContentEnrichment()); + + Log = log; } public async Task FindAssetAsync(DomainId id) @@ -119,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private IDataLoader GetAssetsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader(nameof(GetAssetsLoader), + return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetAssetsLoader), async batch => { var result = await GetReferencedAssetsAsync(new List(batch)); @@ -130,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private IDataLoader GetContentsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader(nameof(GetContentsLoader), + return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetContentsLoader), async batch => { var result = await GetReferencedContentsAsync(new List(batch)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index ac8c73812..eb3d45f38 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -12,7 +12,6 @@ using GraphQL; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; using Squidex.Log; using GraphQLSchema = GraphQL.Types.Schema; @@ -31,18 +30,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL schema = new Builder(app, typeFactory).BuildSchema(schemas); } - public async Task<(object Data, object[]? Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) + public async Task ExecuteAsync(ExecutionOptions options) { - Guard.NotNull(context, nameof(context)); + options.Schema = schema; - var result = await Executor.ExecuteAsync(execution => - { - context.Setup(execution); - - execution.Schema = schema; - execution.Inputs = query.Inputs; - execution.Query = query.Query; - }); + var result = await Executor.ExecuteAsync(options); if (result.Errors != null && result.Errors.Any()) { @@ -58,9 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL })); } - var errors = result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray(); - - return (result.Data, errors); + return result; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs index 65760a321..6563784f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs @@ -6,13 +6,12 @@ // ========================================================================== using System.Threading.Tasks; +using GraphQL; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public interface IGraphQLService { - Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries); - - Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query); + Task ExecuteAsync(ExecutionOptions options); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs index 3704b9c66..0377a001e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { foreach (var schemaInfo in schemas.Where(x => x.Fields.Count > 0)) { - var contentType = builder.GetContentType(schemaInfo); + var contentType = new NonNullGraphType(builder.GetContentType(schemaInfo)); var inputType = new DataInputGraphType(builder, schemaInfo); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index d951b2062..dcde7900c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -207,11 +207,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsCreate, c => { - var contentPublish = c.GetArgument("publish"); + var publish = c.GetArgument("publish"); var contentData = GetContentData(c); var contentId = c.GetArgument("id"); - var command = new CreateContent { Data = contentData, Publish = contentPublish }; + var command = new CreateContent { Data = contentData, Publish = publish }; if (!string.IsNullOrWhiteSpace(contentId)) { @@ -263,13 +263,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents public static readonly IFieldResolver Resolver = ResolveAsync(Permissions.AppContentsUpsert, c => { - var contentPublish = c.GetArgument("publish"); + var publish = c.GetArgument("publish"); + var contentData = GetContentData(c); var contentId = c.GetArgument("id"); var id = DomainId.Create(contentId); - return new UpsertContent { ContentId = id, Data = contentData, Publish = contentPublish }; + return new UpsertContent { ContentId = id, Data = contentData, Publish = publish }; }); } diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index de60b7b72..f06969b1d 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -27,7 +27,7 @@ - + diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs b/backend/src/Squidex.Web/GraphQL/DummySchema.cs similarity index 53% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs rename to backend/src/Squidex.Web/GraphQL/DummySchema.cs index b25276d16..9c18613cd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs +++ b/backend/src/Squidex.Web/GraphQL/DummySchema.cs @@ -1,20 +1,19 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GraphQL; +using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +namespace Squidex.Web.GraphQL { - public sealed class GraphQLQuery + public sealed class DummySchema : Schema { - public string OperationName { get; set; } - - public string Query { get; set; } - - public Inputs? Inputs { get; set; } + public DummySchema() + { + Query = new ObjectGraphType(); + } } } diff --git a/backend/src/Squidex.Web/GraphQL/DynamicExecutor.cs b/backend/src/Squidex.Web/GraphQL/DynamicExecutor.cs new file mode 100644 index 000000000..1f9ddcffa --- /dev/null +++ b/backend/src/Squidex.Web/GraphQL/DynamicExecutor.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using GraphQL; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; + +namespace Squidex.Web.GraphQL +{ + public sealed class DynamicExecutor : IDocumentExecuter + { + private readonly IGraphQLService graphQLService; + + public DynamicExecutor(IGraphQLService graphQLService) + { + this.graphQLService = graphQLService; + } + + public Task ExecuteAsync(ExecutionOptions options) + { + return graphQLService.ExecuteAsync(options); + } + } +} diff --git a/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs b/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs new file mode 100644 index 000000000..7a7840372 --- /dev/null +++ b/backend/src/Squidex.Web/GraphQL/DynamicUserContextBuilder.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using GraphQL.Server.Transports.AspNetCore; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; + +namespace Squidex.Web.GraphQL +{ + public sealed class DynamicUserContextBuilder : IUserContextBuilder + { + private readonly ObjectFactory factory = ActivatorUtilities.CreateFactory(typeof(GraphQLExecutionContext), new[] { typeof(Context) }); + + public Task> BuildUserContext(HttpContext httpContext) + { + var executionContext = (GraphQLExecutionContext)factory(httpContext.RequestServices, new object[] { httpContext.Context() }); + + return Task.FromResult>(executionContext); + } + } +} diff --git a/backend/src/Squidex.Web/GraphQL/GraphQLMiddleware.cs b/backend/src/Squidex.Web/GraphQL/GraphQLMiddleware.cs new file mode 100644 index 000000000..e437303c2 --- /dev/null +++ b/backend/src/Squidex.Web/GraphQL/GraphQLMiddleware.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using GraphQL.Server.Transports.AspNetCore; +using GraphQL.Server.Transports.AspNetCore.Common; +using Microsoft.AspNetCore.Http; + +namespace Squidex.Web.GraphQL +{ + public sealed class GraphQLMiddleware : GraphQLHttpMiddleware + { + private static readonly RequestDelegate Noop = _ => Task.CompletedTask; + + public GraphQLMiddleware(IGraphQLRequestDeserializer deserializer) + : base(Noop, default, deserializer) + { + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index 79a2f8180..bab2cff5d 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -10,24 +10,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; namespace Squidex.Web.Pipeline { public sealed class CachingFilter : IAsyncActionFilter { - private readonly CachingOptions cachingOptions; private readonly CachingManager cachingManager; - public CachingFilter(CachingManager cachingManager, IOptions cachingOptions) + public CachingFilter(CachingManager cachingManager) { Guard.NotNull(cachingManager, nameof(cachingManager)); - Guard.NotNull(cachingOptions, nameof(cachingOptions)); - this.cachingOptions = cachingOptions.Value; this.cachingManager = cachingManager; } @@ -35,26 +30,15 @@ namespace Squidex.Web.Pipeline { cachingManager.Start(context.HttpContext); - AppendAuthHeaders(context.HttpContext); - var resultContext = await next(); if (resultContext.HttpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) { - if (!cachingOptions.StrongETag && IsWeakEtag(etag)) - { - etag = ToWeakEtag(etag); - - resultContext.HttpContext.Response.Headers[HeaderNames.ETag] = etag; - } - if (IsCacheable(resultContext.HttpContext, etag)) { resultContext.Result = new StatusCodeResult(304); } } - - cachingManager.Finish(resultContext.HttpContext); } private static bool IsCacheable(HttpContext httpContext, string etag) @@ -64,29 +48,5 @@ namespace Squidex.Web.Pipeline httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) && string.Equals(etag, noneMatch, StringComparison.Ordinal); } - - private void AppendAuthHeaders(HttpContext httpContext) - { - cachingManager.AddHeader("Auth-State"); - - if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject())) - { - cachingManager.AddHeader(HeaderNames.Authorization); - } - else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId())) - { - cachingManager.AddHeader("Auth-ClientId"); - } - } - - private static string ToWeakEtag(string? etag) - { - return $"W/{etag}"; - } - - private static bool IsWeakEtag(string etag) - { - return !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase); - } } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs b/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs new file mode 100644 index 000000000..9cd742a71 --- /dev/null +++ b/backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; + +namespace Squidex.Web.Pipeline +{ + public sealed class CachingKeysMiddleware + { + private readonly CachingOptions cachingOptions; + private readonly CachingManager cachingManager; + private readonly RequestDelegate next; + + public CachingKeysMiddleware(CachingManager cachingManager, IOptions cachingOptions, RequestDelegate next) + { + Guard.NotNull(cachingManager, nameof(cachingManager)); + Guard.NotNull(cachingOptions, nameof(cachingOptions)); + Guard.NotNull(next, nameof(next)); + + this.cachingOptions = cachingOptions.Value; + this.cachingManager = cachingManager; + + this.next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + cachingManager.Start(context); + + AppendAuthHeaders(context); + + context.Response.OnStarting(x => + { + var httpContext = (HttpContext)x; + + if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) + { + if (!cachingOptions.StrongETag && IsWeakEtag(etag)) + { + httpContext.Response.Headers[HeaderNames.ETag] = ToWeakEtag(etag); + } + } + + cachingManager.Finish(httpContext); + + return Task.CompletedTask; + }, context); + + await next(context); + } + + private void AppendAuthHeaders(HttpContext httpContext) + { + cachingManager.AddHeader("Auth-State"); + + if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject())) + { + cachingManager.AddHeader(HeaderNames.Authorization); + } + else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId())) + { + cachingManager.AddHeader("Auth-ClientId"); + } + } + + private static string ToWeakEtag(string? etag) + { + return $"W/{etag}"; + } + + private static bool IsWeakEtag(string etag) + { + return !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index a89fe7b22..4dc6e95ec 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -231,7 +231,7 @@ namespace Squidex.Web.Pipeline { Guard.NotNull(httpContext, nameof(httpContext)); - var cacheContext = httpContextAccessor.HttpContext?.Features.Get(); + var cacheContext = httpContext.Features.Get(); cacheContext?.Finish(httpContext.Response, stringBuilderPool); } diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index e913c3f2e..a56e05544 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index f8a13660e..2d16b5794 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -14,11 +14,11 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; +using Squidex.Web.GraphQL; namespace Squidex.Areas.Api.Controllers.Contents { @@ -26,25 +26,24 @@ namespace Squidex.Areas.Api.Controllers.Contents { private readonly IContentQueryService contentQuery; private readonly IContentWorkflow contentWorkflow; - private readonly IGraphQLService graphQl; + private readonly GraphQLMiddleware graphQLMiddleware; public ContentsController(ICommandBus commandBus, IContentQueryService contentQuery, IContentWorkflow contentWorkflow, - IGraphQLService graphQl) + GraphQLMiddleware graphQLMiddleware) : base(commandBus) { this.contentQuery = contentQuery; this.contentWorkflow = contentWorkflow; - this.graphQl = graphQl; + this.graphQLMiddleware = graphQLMiddleware; } /// /// GraphQL endpoint. /// /// The name of the app. - /// The graphql query. /// /// 200 => Contents returned or mutated. /// 404 => App not found. @@ -53,87 +52,14 @@ namespace Squidex.Areas.Api.Controllers.Contents /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [HttpGet] - [Route("content/{app}/graphql/")] - [ApiPermissionOrAnonymous] - [ApiCosts(2)] - public async Task GetGraphQL(string app, [FromQuery] GraphQLGetDto? queries = null) - { - var request = queries?.ToQuery() ?? new GraphQLQuery(); - - var (hasError, response) = await graphQl.QueryAsync(Context, request); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// GraphQL endpoint. - /// - /// The name of the app. - /// The graphql query. - /// - /// 200 => Contents returned or mutated. - /// 404 => App not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// [HttpPost] [Route("content/{app}/graphql/")] - [ApiPermissionOrAnonymous] - [ApiCosts(2)] - public async Task PostGraphQL(string app, [FromBody] GraphQLPostDto query) - { - var request = query?.ToQuery() ?? new GraphQLQuery(); - - var (hasError, response) = await graphQl.QueryAsync(Context, request); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } - } - - /// - /// GraphQL endpoint (Batch). - /// - /// The name of the app. - /// The graphql queries. - /// - /// 200 => Contents returned or mutated. - /// 404 => App not found. - /// - /// - /// You can read the generated documentation for your app at /api/content/{appName}/docs. - /// - [HttpPost] [Route("content/{app}/graphql/batch")] [ApiPermissionOrAnonymous] [ApiCosts(2)] - public async Task PostGraphQLBatch(string app, [FromBody] GraphQLPostDto[] batch) + public Task GetGraphQL(string app) { - var request = batch.Select(x => x.ToQuery()).ToArray(); - - var (hasError, response) = await graphQl.QueryAsync(Context, request); - - if (hasError) - { - return BadRequest(response); - } - else - { - return Ok(response); - } + return graphQLMiddleware.InvokeAsync(HttpContext); } /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs deleted file mode 100644 index e8b2c574f..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.NewtonsoftJson; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public class GraphQLGetDto - { - public string OperationName { get; set; } - - public string Query { get; set; } - - public string Variables { get; set; } - - public GraphQLQuery ToQuery() - { - var query = SimpleMapper.Map(this, new GraphQLQuery()); - - query.Inputs = Variables?.ToInputs(); - - return query; - } - } -} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs deleted file mode 100644 index efb19cc8e..000000000 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs +++ /dev/null @@ -1,32 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using GraphQL.NewtonsoftJson; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Areas.Api.Controllers.Contents.Models -{ - public class GraphQLPostDto - { - public string OperationName { get; set; } - - public string Query { get; set; } - - public JObject Variables { get; set; } - - public GraphQLQuery ToQuery() - { - var query = SimpleMapper.Map(this, new GraphQLQuery()); - - query.Inputs = Variables?.ToInputs(); - - return query; - } - } -} diff --git a/backend/src/Squidex/Config/Authentication/IdentityServices.cs b/backend/src/Squidex/Config/Authentication/IdentityServices.cs index d4d4c0f3e..51be38e98 100644 --- a/backend/src/Squidex/Config/Authentication/IdentityServices.cs +++ b/backend/src/Squidex/Config/Authentication/IdentityServices.cs @@ -16,7 +16,8 @@ namespace Squidex.Config.Authentication { public static void AddSquidexIdentity(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "identity"); + services.Configure(config, + "identity"); services.AddSingletonAs() .AsOptional(); diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index f1f35f3cf..191ee1062 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -29,7 +29,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexAssets(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "assets"); + services.Configure(config, + "assets"); if (config.GetValue("assets:deleteRecursive")) { diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index f614b721c..35a5688ca 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -32,7 +32,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexCommands(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "mode"); + services.Configure(config, + "mode"); services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index e8ef3582d..8db844c9d 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -27,7 +27,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexContents(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "contents"); + services.Configure(config, + "contents"); services.AddSingletonAs(c => new Lazy(c.GetRequiredService)) .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs index 64452862c..8feba574c 100644 --- a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs +++ b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs @@ -17,7 +17,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexHealthChecks(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "healthz:gc"); + services.Configure(config, + "healthz:gc"); services.AddHealthChecks() .AddCheck("GC", tags: new[] { "node" }) diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index d4e7f8835..25086395d 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -29,8 +29,10 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline.Robots; +using Squidex.Shared; using Squidex.Text.Translations; using Squidex.Text.Translations.GoogleCloud; using Squidex.Web; @@ -42,9 +44,11 @@ namespace Squidex.Config.Domain { public static void AddSquidexInfrastructure(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "exposedConfiguration"); + services.Configure(config, + "exposedConfiguration"); - services.Configure(config, "caching:replicated"); + services.Configure(config, + "caching:replicated"); services.AddReplicatedCache(); services.AddAsyncLocalCache(); @@ -100,7 +104,8 @@ namespace Squidex.Config.Domain public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "usage"); + services.Configure(config, + "usage"); services.AddSingletonAs(c => new CachingUsageTracker( c.GetRequiredService(), @@ -119,11 +124,14 @@ namespace Squidex.Config.Domain public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "translations:googleCloud"); + services.Configure(config, + "translations:googleCloud"); - services.Configure(config, "translations:deepL"); + services.Configure(config, + "translations:deepL"); - services.Configure(config, "languages"); + services.Configure(config, + "languages"); services.AddSingletonAs() .AsSelf(); @@ -138,15 +146,29 @@ namespace Squidex.Config.Domain .As(); } + public static void AddSquidexLocalization(this IServiceCollection services) + { + var translator = new ResourcesLocalizer(Texts.ResourceManager); + + T.Setup(translator); + + services.AddSingletonAs(c => translator) + .As(); + } + public static void AddSquidexControllerServices(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "robots"); + services.Configure(config, + "robots"); - services.Configure(config, "caching"); + services.Configure(config, + "caching"); - services.Configure(config, "ui"); + services.Configure(config, + "ui"); - services.Configure(config, "news"); + services.Configure(config, + "news"); services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs index d2d5fb3f7..26f82ac87 100644 --- a/backend/src/Squidex/Config/Domain/LoggingServices.cs +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -35,9 +35,11 @@ namespace Squidex.Config.Domain private static void AddServices(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "logging"); + services.Configure(config, + "logging"); - services.Configure(config, "logging"); + services.Configure(config, + "logging"); services.AddSingletonAs(_ => new ApplicationInfoLogAppender(typeof(LoggingServices).Assembly, Guid.NewGuid())) .As(); diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs index 8bf642238..f3f47142f 100644 --- a/backend/src/Squidex/Config/Domain/MigrationServices.cs +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -17,7 +17,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexMigration(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "rebuild"); + services.Configure(config, + "rebuild"); services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/NotificationsServices.cs b/backend/src/Squidex/Config/Domain/NotificationsServices.cs index 8af672216..e36acc613 100644 --- a/backend/src/Squidex/Config/Domain/NotificationsServices.cs +++ b/backend/src/Squidex/Config/Domain/NotificationsServices.cs @@ -25,7 +25,8 @@ namespace Squidex.Config.Domain { services.AddSingleton(Options.Create(emailOptions)); - services.Configure(config, "email:notifications"); + services.Configure(config, + "email:notifications"); services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs index e482beb5a..90fe106e6 100644 --- a/backend/src/Squidex/Config/Domain/QueryServices.cs +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GraphQL.DataLoader; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core; @@ -21,20 +20,12 @@ namespace Squidex.Config.Domain { var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - services.Configure(config, "graphql"); + services.Configure(config, + "graphql"); services.AddSingletonAs(c => ActivatorUtilities.CreateInstance(c, exposeSourceUrl)) .As(); - services.AddSingletonAs() - .As(); - - services.AddTransientAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index ad88590b3..1b4bfd5e9 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -30,7 +30,8 @@ namespace Squidex.Config.Domain { public static void AddSquidexRules(this IServiceCollection services, IConfiguration config) { - services.Configure(config, "rules"); + services.Configure(config, + "rules"); services.AddTransientAs() .AsSelf(); diff --git a/backend/src/Squidex/Config/Domain/SerializationServices.cs b/backend/src/Squidex/Config/Domain/SerializationServices.cs index 3f44c2199..a0e0deda2 100644 --- a/backend/src/Squidex/Config/Domain/SerializationServices.cs +++ b/backend/src/Squidex/Config/Domain/SerializationServices.cs @@ -5,6 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using GraphQL; +using GraphQL.Execution; +using GraphQL.NewtonsoftJson; +using GraphQL.Server; using Microsoft.Extensions.DependencyInjection; using Migrations; using Newtonsoft.Json; @@ -15,6 +19,7 @@ using Squidex.Domain.Apps.Core.Contents.Json; using Squidex.Domain.Apps.Core.Rules.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; @@ -38,6 +43,7 @@ namespace Squidex.Config.Domain new ContentFieldDataConverter(), new DomainIdConverter(), new EnvelopeHeadersConverter(), + new ExecutionResultJsonConverter(new ErrorInfoProvider()), new FilterConverter(), new InstantConverter(), new JsonValueConverter(), @@ -115,16 +121,29 @@ namespace Squidex.Config.Domain return services; } - public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder mvc) + public static IMvcBuilder AddSquidexSerializers(this IMvcBuilder builder) { - mvc.AddNewtonsoftJson(options => + builder.AddNewtonsoftJson(options => { options.AllowInputFormatterExceptionMessages = false; ConfigureJson(options.SerializerSettings, TypeNameHandling.None); }); - return mvc; + return builder; + } + + public static IGraphQLBuilder AddSquidexWriter(this IGraphQLBuilder builder) + { + builder.Services.AddSingleton(c => + { + var settings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.None); + var serializer = new NewtonsoftJsonSerializer(settings); + + return new DefaultDocumentWriter(serializer); + }); + + return builder; } } } diff --git a/backend/src/Squidex/Config/Web/WebExtensions.cs b/backend/src/Squidex/Config/Web/WebExtensions.cs index a064b9e34..bc317799d 100644 --- a/backend/src/Squidex/Config/Web/WebExtensions.cs +++ b/backend/src/Squidex/Config/Web/WebExtensions.cs @@ -23,6 +23,13 @@ namespace Squidex.Config.Web { public static class WebExtensions { + public static IApplicationBuilder UseSquidexCacheKeys(this IApplicationBuilder app) + { + app.UseMiddleware(); + + return app; + } + public static IApplicationBuilder UseSquidexLocalCache(this IApplicationBuilder app) { app.UseMiddleware(); diff --git a/backend/src/Squidex/Config/Web/WebServices.cs b/backend/src/Squidex/Config/Web/WebServices.cs index 625d2b3ff..511733448 100644 --- a/backend/src/Squidex/Config/Web/WebServices.cs +++ b/backend/src/Squidex/Config/Web/WebServices.cs @@ -5,6 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using GraphQL; +using GraphQL.Server; +using GraphQL.Server.Transports.AspNetCore; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -14,11 +17,11 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Squidex.Config.Domain; using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Caching; -using Squidex.Infrastructure.Translations; using Squidex.Pipeline.Plugins; -using Squidex.Shared; using Squidex.Web; +using Squidex.Web.GraphQL; using Squidex.Web.Pipeline; using Squidex.Web.Services; @@ -28,10 +31,6 @@ namespace Squidex.Config.Web { public static void AddSquidexMvcWithPlugins(this IServiceCollection services, IConfiguration config) { - var translator = new ResourcesLocalizer(Texts.ResourceManager); - - T.Setup(translator); - services.AddDefaultWebServices(config); services.AddDefaultForwardRules(); @@ -53,9 +52,6 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs(c => translator) - .As(); - services.AddSingletonAs() .As().As(); @@ -91,5 +87,28 @@ namespace Squidex.Config.Web .AddSquidexPlugins(config) .AddSquidexSerializers(); } + + public static void AddSquidexGraphQL(this IServiceCollection services) + { + services.AddGraphQL(options => + { + options.EnableMetrics = false; + }) + .AddDataLoader() + .AddSystemTextJson() + .AddSquidexWriter(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + } } } diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 911a3a009..30b6d2042 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -34,7 +34,8 @@ - + + @@ -64,7 +65,7 @@ - + diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 585ecee2e..d62952a10 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -51,11 +51,13 @@ namespace Squidex services.AddSquidexControllerServices(config); services.AddSquidexEventPublisher(config); services.AddSquidexEventSourcing(config); + services.AddSquidexGraphQL(); services.AddSquidexHealthChecks(config); services.AddSquidexHistory(config); services.AddSquidexIdentity(config); services.AddSquidexIdentityServer(); services.AddSquidexInfrastructure(config); + services.AddSquidexLocalization(); services.AddSquidexMigration(config); services.AddSquidexNotifications(config); services.AddSquidexOpenApiSettings(); @@ -76,6 +78,7 @@ namespace Squidex app.UseDefaultForwardRules(); + app.UseSquidexCacheKeys(); app.UseSquidexHealthCheck(); app.UseSquidexRobotsTxt(); app.UseSquidexTracking(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs index 7c69d94da..ea49b0dff 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestUtils.cs @@ -32,7 +32,8 @@ namespace Squidex.Domain.Apps.Core.TestHelpers public static readonly JsonSerializerSettings DefaultSerializerSettings = CreateSerializerSettings(); - public static JsonSerializerSettings CreateSerializerSettings(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) + public static JsonSerializerSettings CreateSerializerSettings(TypeNameHandling typeNameHandling = TypeNameHandling.Auto, + JsonConverter? converter = null) { var typeNameRegistry = new TypeNameRegistry() @@ -76,12 +77,17 @@ namespace Squidex.Domain.Apps.Core.TestHelpers TypeNameHandling = typeNameHandling }; + if (converter != null) + { + serializerSettings.Converters.Add(converter); + } + return serializerSettings; } - public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto) + public static IJsonSerializer CreateSerializer(TypeNameHandling typeNameHandling = TypeNameHandling.Auto, JsonConverter? converter = null) { - var serializerSettings = CreateSerializerSettings(typeNameHandling); + var serializerSettings = CreateSerializerSettings(typeNameHandling, converter); return new NewtonsoftJsonSerializer(serializerSettings); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs index 4b8776d93..142f43c1e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Threading.Tasks; +using GraphQL; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL @@ -94,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"; - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query, OperationName = "IntrospectionQuery" }); - var json = serializer.Serialize(result.Response, true); + var json = serializer.Serialize(result.Data, true); Assert.NotEmpty(json); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index e2a51a216..7593f1027 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -15,7 +15,6 @@ using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; @@ -47,14 +46,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }"; - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = new - { - createMySchemaContent = (object?)null - }, errors = new[] { new @@ -67,6 +62,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "createMySchemaContent" } } } @@ -90,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsCreate); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsCreate); var expected = new { @@ -123,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsCreate); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsCreate); var expected = new { @@ -157,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsCreate); + var result = await ExecuteAsync( new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsCreate); var expected = new { @@ -188,14 +187,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = new - { - updateMySchemaContent = (object?)null - }, errors = new[] { new @@ -208,6 +203,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "updateMySchemaContent" } } } @@ -231,7 +230,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpdateOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsUpdateOwn); var expected = new { @@ -263,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); var expected = new { @@ -293,14 +292,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = new - { - upsertMySchemaContent = (object?)null - }, errors = new[] { new @@ -313,6 +308,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "upsertMySchemaContent" } } } @@ -336,7 +335,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpsert); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsUpsert); var expected = new { @@ -369,7 +368,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpsert); + var result = await ExecuteAsync( new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpsert); var expected = new { @@ -400,14 +399,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = new - { - patchMySchemaContent = (object?)null - }, errors = new[] { new @@ -420,6 +415,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "patchMySchemaContent" } } } @@ -443,7 +442,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsUpdateOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsUpdateOwn); var expected = new { @@ -475,7 +474,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query, Inputs = GetInput() }, Permissions.AppContentsUpdateOwn); var expected = new { @@ -505,14 +504,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = new - { - changeMySchemaContent = (object?)null - }, errors = new[] { new @@ -525,6 +520,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "changeMySchemaContent" } } } @@ -550,7 +549,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { @@ -583,7 +582,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { @@ -616,7 +615,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(content); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsChangeStatusOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { @@ -647,11 +646,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - var result = await ExecuteAsync(new GraphQLQuery { Query = query }, Permissions.AppContentsReadOwn); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { - data = (object?)null, errors = new[] { new @@ -664,6 +662,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL line = 3, column = 19 } + }, + path = new[] + { + "deleteMySchemaContent" } } } @@ -687,7 +689,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL commandContext.Complete(new EntitySavedResult(13)); - var result = await ExecuteAsync( new GraphQLQuery { Query = query }, Permissions.AppContentsDeleteOwn); + var result = await ExecuteAsync( new ExecutionOptions { Query = query }, Permissions.AppContentsDeleteOwn); var expected = new { @@ -709,15 +711,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .MustHaveHappened(); } - private Task<(bool HasError, object Response)> ExecuteAsync(GraphQLQuery query, string permissionId) - { - var permission = Permissions.ForApp(permissionId, app.Name, schemaId.Name).Id; - - var withPermission = new Context(Mocks.FrontendUser(permission: permission), app); - - return sut.QueryAsync(withPermission, query); - } - private Inputs GetInput() { var input = new diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 840f085a5..3a13607ed 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using FakeItEasy; +using GraphQL; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -18,17 +19,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public class GraphQLQueriesTests : GraphQLTestBase { [Theory] - [InlineData(null)] [InlineData("")] [InlineData(" ")] - public async Task Should_return_empty_object_for_empty_query(string query) + public async Task Should_return_error_empty_query(string query) { - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { - data = new + errors = new object[] { + new + { + message = "Document does not contain any operations.", + extensions = new + { + code = "NO_OPERATION", + codes = new[] + { + "NO_OPERATION" + } + } + } } }; @@ -51,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, asset)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -86,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query" && x.NoTotal == false))) .Returns(ResultList.CreateFrom(10, asset)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -121,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId))) .Returns(ResultList.CreateFrom(1)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -150,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId))) .Returns(ResultList.CreateFrom(1, asset)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -202,7 +214,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -281,7 +293,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == true))) .Returns(ResultList.CreateFrom(0, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -316,7 +328,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.NoTotal == false))) .Returns(ResultList.CreateFrom(10, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -351,7 +363,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -380,7 +392,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -409,7 +421,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), schemaId.Id.ToString(), contentId, 3)) .Returns(content); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -456,7 +468,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -523,7 +535,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == true))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -587,7 +599,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5" && x.Reference == contentRefId && x.NoTotal == false))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -660,7 +672,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -726,7 +738,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetRefId))) .Returns(ResultList.CreateFrom(0, assetRef)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var expected = new { @@ -755,62 +767,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } - [Fact] - public async Task Should_make_multiple_queries() - { - var assetId1 = DomainId.NewGuid(); - var assetId2 = DomainId.NewGuid(); - var asset1 = TestAsset.Create(appId, assetId1); - var asset2 = TestAsset.Create(appId, assetId2); - - var query1 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId1.ToString()); - var query2 = @" - query { - findAsset(id: """") { - id - } - }".Replace("", assetId2.ToString()); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId1))) - .Returns(ResultList.CreateFrom(0, asset1)); - - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A.That.HasIdsWithoutTotal(assetId2))) - .Returns(ResultList.CreateFrom(0, asset2)); - - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); - - var expected = new object[] - { - new - { - data = new - { - findAsset = new - { - id = asset1.Id - } - } - }, - new - { - data = new - { - findAsset = new - { - id = asset2.Id - } - } - } - }; - - AssertResult(expected, result); - } - [Fact] public async Task Should_not_return_data_when_field_not_part_of_content() { @@ -838,11 +794,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId))) .Returns(ResultList.CreateFrom(1, content)); - var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + var result = await ExecuteAsync(new ExecutionOptions { Query = query }); var json = serializer.Serialize(result); - Assert.Contains("\"data\":null", json); + Assert.Contains("\"errors\"", json); } private Context MatchsAssetContext() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index c9d7d974c..df1bf7151 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -5,10 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; +using System.Threading.Tasks; using FakeItEasy; +using GraphQL; using GraphQL.DataLoader; +using GraphQL.Execution; +using GraphQL.NewtonsoftJson; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -27,6 +30,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json; using Squidex.Log; +using Squidex.Shared; using Xunit; #pragma warning disable SA1401 // Fields must be private @@ -35,22 +39,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLTestBase : IClassFixture { - protected readonly IAppEntity app; + protected readonly IJsonSerializer serializer = + TestUtils.CreateSerializer(TypeNameHandling.None, + new ExecutionResultJsonConverter(new ErrorInfoProvider())); protected readonly IAssetQueryService assetQuery = A.Fake(); protected readonly ICommandBus commandBus = A.Fake(); protected readonly IContentQueryService contentQuery = A.Fake(); - protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); protected readonly ISchemaEntity schema; protected readonly ISchemaEntity schemaRef1; protected readonly ISchemaEntity schemaRef2; protected readonly ISchemaEntity schemaInvalidName; + protected readonly IAppEntity app; protected readonly Context requestContext; protected readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); protected readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); protected readonly NamedId schemaRefId1 = NamedId.Of(DomainId.NewGuid(), "my-ref-schema1"); protected readonly NamedId schemaRefId2 = NamedId.Of(DomainId.NewGuid(), "my-ref-schema2"); protected readonly NamedId schemaInvalidNameId = NamedId.Of(DomainId.NewGuid(), "content"); - protected readonly IGraphQLService sut; + protected readonly CachingGraphQLService sut; public GraphQLTestBase() { @@ -123,22 +129,40 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL sut = CreateSut(); } - protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true) + protected void AssertResult(object expected, ExecutionResult result) { - if (checkErrors && result.HasErrors) - { - throw new InvalidOperationException(Serialize(result)); - } - - var resultJson = serializer.Serialize(result.Response, true); + var resultJson = serializer.Serialize(result, true); var expectJson = serializer.Serialize(expected, true); Assert.Equal(expectJson, resultJson); } - private string Serialize((bool HasErrors, object Response) result) + protected Task ExecuteAsync(ExecutionOptions options, string? permissionId = null) + { + var context = requestContext; + + if (permissionId != null) + { + var permission = Permissions.ForApp(permissionId, app.Name, schemaId.Name).Id; + + context = new Context(Mocks.FrontendUser(permission: permission), app); + } + + return ExcecuteAsync(options, context); + } + + private Task ExcecuteAsync(ExecutionOptions options, Context context) { - return serializer.Serialize(result); + options.UserContext = ActivatorUtilities.CreateInstance(sut.Services, context); + + var listener = sut.Services.GetService(); + + if (listener != null) + { + options.Listeners.Add(listener); + } + + return sut.ExecuteAsync(options); } private CachingGraphQLService CreateSut() diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs index 9fc0b052c..7318cd145 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs @@ -39,6 +39,11 @@ namespace Squidex.Infrastructure.Commands { throw new NotSupportedException(); } + + public ISemanticLog CreateScope(ILogAppender appender) + { + throw new NotSupportedException(); + } } public LogCommandMiddlewareTests() diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs index 5a783a01b..6489b952e 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; using Microsoft.AspNetCore.Http; @@ -15,10 +14,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; using Xunit; namespace Squidex.Web.Pipeline @@ -49,138 +45,7 @@ namespace Squidex.Web.Pipeline Result = new OkResult() }; - sut = new CachingFilter(cachingManager, Options.Create(cachingOptions)); - } - - [Fact] - public async Task Should_not_append_etag_if_not_found() - { - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_append_authorization_header_as_vary() - { - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_append_authorization_as_header_when_user_has_subject() - { - var identity = (ClaimsIdentity)httpContext.User.Identity!; - - identity.AddClaim(new Claim(OpenIdClaims.Subject, "my-id")); - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("Auth-State,Authorization", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_append_client_id_as_header_when_user_has_client_but_no_subject() - { - var identity = (ClaimsIdentity)httpContext.User.Identity!; - - identity.AddClaim(new Claim(OpenIdClaims.ClientId, "my-client")); - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("Auth-State,Auth-ClientId", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_not_append_null_header_as_vary() - { - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddHeader(null!); - - return Task.FromResult(executedContext); - }); - - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_not_append_empty_header_as_vary() - { - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddHeader(string.Empty); - - return Task.FromResult(executedContext); - }); - - Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_append_custom_header_as_vary() - { - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddHeader("X-Header"); - - return Task.FromResult(executedContext); - }); - - Assert.Equal("Auth-State,X-Header", httpContext.Response.Headers[HeaderNames.Vary]); - } - - [Fact] - public async Task Should_not_append_etag_if_empty() - { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_not_convert_strong_etag_if_disabled() - { - cachingOptions.StrongETag = true; - - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_not_convert_already_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_convert_strong_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = "13"; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); - } - - [Fact] - public async Task Should_not_convert_empty_string_to_weak_tag() - { - httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; - - await sut.OnActionExecutionAsync(executingContext, Next()); - - Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + sut = new CachingFilter(cachingManager); } [Fact] @@ -189,7 +54,7 @@ namespace Squidex.Web.Pipeline httpContext.Request.Method = HttpMethods.Get; httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - httpContext.Response.Headers[HeaderNames.ETag] = "13"; + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; await sut.OnActionExecutionAsync(executingContext, Next()); @@ -200,129 +65,15 @@ namespace Squidex.Web.Pipeline public async Task Should_not_return_304_for_different_etags() { httpContext.Request.Method = HttpMethods.Get; - httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/11"; + httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13"; - httpContext.Response.Headers[HeaderNames.ETag] = "13"; + httpContext.Response.Headers[HeaderNames.ETag] = "W/11"; await sut.OnActionExecutionAsync(executingContext, Next()); Assert.Equal(200, ((StatusCodeResult)executedContext.Result).StatusCode); } - [Fact] - public async Task Should_append_surrogate_keys() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - cachingOptions.MaxSurrogateKeysSize = 100; - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - - return Task.FromResult(executedContext); - }); - - Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]); - } - - [Fact] - public async Task Should_append_surrogate_keys_if_just_enough_space_for_one() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - cachingOptions.MaxSurrogateKeysSize = 36; - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - - return Task.FromResult(executedContext); - }); - - Assert.Equal($"{id1}", httpContext.Response.Headers["Surrogate-Key"]); - } - - [Fact] - public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - cachingOptions.MaxSurrogateKeysSize = 20; - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - - return Task.FromResult(executedContext); - }); - - Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); - } - - [Fact] - public async Task Should_not_append_surrogate_keys_if_maximum_is_overriden() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - httpContext.Request.Headers[CachingManager.SurrogateKeySizeHeader] = "20"; - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - - return Task.FromResult(executedContext); - }); - - Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); - } - - [Fact] - public async Task Should_generate_etag_from_ids_and_versions() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - cachingManager.AddDependency(12); - - return Task.FromResult(executedContext); - }); - - Assert.True(httpContext.Response.Headers[HeaderNames.ETag].ToString().Length > 20); - } - - [Fact] - public async Task Should_not_generate_etag_when_already_added() - { - var id1 = DomainId.NewGuid(); - var id2 = DomainId.NewGuid(); - - await sut.OnActionExecutionAsync(executingContext, () => - { - cachingManager.AddDependency(id1, 12); - cachingManager.AddDependency(id2, 12); - cachingManager.AddDependency(12); - - executedContext.HttpContext.Response.Headers[HeaderNames.ETag] = "W/20"; - - return Task.FromResult(executedContext); - }); - - Assert.Equal("W/20", httpContext.Response.Headers[HeaderNames.ETag]); - } - private ActionExecutionDelegate Next() { return () => Task.FromResult(executedContext); diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs new file mode 100644 index 000000000..43dddac49 --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs @@ -0,0 +1,328 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class CachingKeysMiddlewareTests + { + private readonly List<(object, Func)> callbacks = new List<(object, Func)>(); + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly IHttpResponseBodyFeature httpResponseBodyFeature = A.Fake(); + private readonly IHttpResponseFeature httpResponseFeature = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly CachingOptions cachingOptions = new CachingOptions(); + private readonly CachingManager cachingManager; + private readonly RequestDelegate next; + private readonly CachingKeysMiddleware sut; + private bool isNextCalled; + + public CachingKeysMiddlewareTests() + { + var headers = new HeaderDictionary(); + + A.CallTo(() => httpResponseFeature.Headers) + .Returns(headers); + + A.CallTo(() => httpResponseFeature.OnStarting(A>._, A._)) + .Invokes(c => + { + callbacks.Add(( + c.GetArgument(1)!, + c.GetArgument>(0)!)); + }); + + A.CallTo(() => httpResponseBodyFeature.StartAsync(A._)) + .Invokes(c => + { + foreach (var (state, callback) in callbacks) + { + callback(state).Wait(); + } + }); + + httpContext.Features.Set(httpResponseBodyFeature); + httpContext.Features.Set(httpResponseFeature); + + next = context => + { + isNextCalled = true; + + return Task.CompletedTask; + }; + + A.CallTo(() => httpContextAccessor.HttpContext) + .Returns(httpContext); + + cachingManager = new CachingManager(httpContextAccessor, Options.Create(cachingOptions)); + + sut = new CachingKeysMiddleware(cachingManager, Options.Create(cachingOptions), next); + } + + [Fact] + public async Task Should_invoke_next() + { + await MakeRequestAsync(); + + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_not_append_etag_if_not_found() + { + await MakeRequestAsync(); + + Assert.Equal(StringValues.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_append_authorization_header_as_vary() + { + await MakeRequestAsync(); + + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_append_authorization_as_header_when_user_has_subject() + { + var identity = (ClaimsIdentity)httpContext.User.Identity!; + + identity.AddClaim(new Claim(OpenIdClaims.Subject, "my-id")); + + await MakeRequestAsync(); + + Assert.Equal("Auth-State,Authorization", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_append_client_id_as_header_when_user_has_client_but_no_subject() + { + var identity = (ClaimsIdentity)httpContext.User.Identity!; + + identity.AddClaim(new Claim(OpenIdClaims.ClientId, "my-client")); + + await MakeRequestAsync(); + + Assert.Equal("Auth-State,Auth-ClientId", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_not_append_null_header_as_vary() + { + await MakeRequestAsync(() => + { + cachingManager.AddHeader(null!); + }); + + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_not_append_empty_header_as_vary() + { + await MakeRequestAsync(() => + { + cachingManager.AddHeader(string.Empty); + }); + + Assert.Equal("Auth-State", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_append_custom_header_as_vary() + { + await MakeRequestAsync(() => + { + cachingManager.AddHeader("X-Header"); + }); + + Assert.Equal("Auth-State,X-Header", httpContext.Response.Headers[HeaderNames.Vary]); + } + + [Fact] + public async Task Should_not_append_etag_if_empty() + { + await MakeRequestAsync(() => + { + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + }); + + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_not_convert_strong_etag_if_disabled() + { + cachingOptions.StrongETag = true; + + await MakeRequestAsync(() => + { + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + }); + + Assert.Equal("13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_not_convert_already_weak_tag() + { + await MakeRequestAsync(() => + { + httpContext.Response.Headers[HeaderNames.ETag] = "W/13"; + }); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_convert_strong_to_weak_tag() + { + await MakeRequestAsync(() => + { + httpContext.Response.Headers[HeaderNames.ETag] = "13"; + }); + + Assert.Equal("W/13", httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_not_convert_empty_string_to_weak_tag() + { + httpContext.Response.Headers[HeaderNames.ETag] = string.Empty; + + await MakeRequestAsync(); + + Assert.Equal(string.Empty, httpContext.Response.Headers[HeaderNames.ETag]); + } + + [Fact] + public async Task Should_append_surrogate_keys() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + cachingOptions.MaxSurrogateKeysSize = 100; + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); + + Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]); + } + + [Fact] + public async Task Should_append_surrogate_keys_if_just_enough_space_for_one() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + cachingOptions.MaxSurrogateKeysSize = 36; + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); + + Assert.Equal($"{id1}", httpContext.Response.Headers["Surrogate-Key"]); + } + + [Fact] + public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + cachingOptions.MaxSurrogateKeysSize = 20; + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); + + Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); + } + + [Fact] + public async Task Should_not_append_surrogate_keys_if_maximum_is_overriden() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + httpContext.Request.Headers[CachingManager.SurrogateKeySizeHeader] = "20"; + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + }); + + Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); + } + + [Fact] + public async Task Should_generate_etag_from_ids_and_versions() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + cachingManager.AddDependency(12); + }); + + Assert.True(httpContext.Response.Headers[HeaderNames.ETag].ToString().Length > 20); + } + + [Fact] + public async Task Should_not_generate_etag_when_already_added() + { + var id1 = DomainId.NewGuid(); + var id2 = DomainId.NewGuid(); + + await MakeRequestAsync(() => + { + cachingManager.AddDependency(DomainId.NewGuid(), 12); + cachingManager.AddDependency(DomainId.NewGuid(), 12); + cachingManager.AddDependency(12); + + httpContext.Response.Headers[HeaderNames.ETag] = "W/20"; + }); + + Assert.Equal("W/20", httpContext.Response.Headers[HeaderNames.ETag]); + } + + private async Task MakeRequestAsync(Action? action = null) + { + await sut.InvokeAsync(httpContext); + + action?.Invoke(); + + await httpContext.Response.StartAsync(); + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 60a400bc6..efe162df0 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -440,6 +440,56 @@ namespace TestSuite.ApiTests Assert.Equal(998, value); } + [Fact] + public async Task Should_batch_query_items_with_graphql() + { + var query1 = new + { + query = @" + query ContentsQuery($filter: String!) { + queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { + id, + data { + number { + iv + } + } + } + }", + variables = new + { + filter = @"data/number/iv gt 3 and data/number/iv lt 7" + } + }; + + var query2 = new + { + query = @" + query ContentsQuery($filter: String!) { + queryMyReadsContents(filter: $filter, orderby: ""data/number/iv asc"") { + id, + data { + number { + iv + } + } + } + }", + variables = new + { + filter = @"data/number/iv gt 4 and data/number/iv lt 7" + } + }; + + var results = await _.Contents.GraphQlAsync(new[] { query1, query2 }); + + var items1 = results.ElementAt(0).Data.Items; + var items2 = results.ElementAt(1).Data.Items; + + Assert.Equal(items1.Select(x => x.Data.Number).ToArray(), new[] { 4, 5, 6 }); + Assert.Equal(items2.Select(x => x.Data.Number).ToArray(), new[] { 5, 6 }); + } + [Fact] public async Task Should_query_items_with_graphql() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index 329119bce..87af19ffb 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -4,11 +4,11 @@ net5.0 - + - + all runtime; build; native; contentfiles; analyzers diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index f8c89425f..de6914d01 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj +++ b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -4,11 +4,11 @@ net5.0 - + - + all runtime; build; native; contentfiles; analyzers diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentReferencesFixture.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentReferencesFixture.cs index 561c57eba..6889fe946 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentReferencesFixture.cs +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/ContentReferencesFixture.cs @@ -33,7 +33,7 @@ namespace TestSuite.Fixtures throw; } } - }).Wait(); + }).Wait(); Contents = ClientManager.CreateContentsClient(SchemaName); } diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index f413f010b..51d13f59f 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -5,13 +5,13 @@ TestSuite - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - +