// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Text.RegularExpressions; using FakeItEasy; using GraphQL; using GraphQL.NewtonsoftJson; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NodaTime.Text; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLMutationTests : GraphQLTestBase { private readonly DomainId contentId = DomainId.NewGuid(); private readonly IEnrichedContentEntity content; private readonly CommandContext commandContext = new CommandContext(new PatchContent(), A.Dummy()); public GraphQLMutationTests() { content = TestContent.Create(contentId, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id, null); A.CallTo(() => commandBus.PublishAsync(A.Ignored)) .Returns(commandContext); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_create() { var query = @" mutation { createMySchemaContent(data: { myNumber: { iv: 42 } }) { id } }"; var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "createMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_single_content_if_creating_content() { var query = CreateQuery(@" mutation { createMySchemaContent(data: , publish: true) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsCreate); var expected = new { data = new { createMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ExpectedVersion == EtagVersion.Any && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_creating_content_with_custom_id() { var query = CreateQuery(@" mutation { createMySchemaContent(data: , id: '123', publish: true) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsCreate); var expected = new { data = new { createMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ExpectedVersion == EtagVersion.Any && x.ContentId == DomainId.Create("123") && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_creating_content_with_variable() { var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { createMySchemaContent(data: $data, publish: true) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsCreate); var expected = new { data = new { createMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ExpectedVersion == EtagVersion.Any && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_update() { var query = CreateQuery(@" mutation { updateMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "updateMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_single_content_if_updating_content() { var query = CreateQuery(@" mutation { updateMySchemaContent(id: '', data: , expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsUpdateOwn); var expected = new { data = new { updateMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_updating_content_with_variable() { var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { updateMySchemaContent(id: '', data: $data, expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpdateOwn); var expected = new { data = new { updateMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_upsert() { var query = CreateQuery(@" mutation { upsertMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "upsertMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_single_content_if_upserting_content() { var query = CreateQuery(@" mutation { upsertMySchemaContent(id: '', data: , publish: true, expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsUpsert); var expected = new { data = new { upsertMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_upserting_content_with_variable() { var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { upsertMySchemaContent(id: '', data: $data, publish: true, expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpsert); var expected = new { data = new { upsertMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_patch() { var query = CreateQuery(@" mutation { patchMySchemaContent(id: '', data: { myNumber: { iv: 42 } }) { id } }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "patchMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_single_content_if_patching_content() { var query = CreateQuery(@" mutation { patchMySchemaContent(id: '', data: , expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsUpdateOwn); var expected = new { data = new { patchMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_patching_content_with_variable() { var query = CreateQuery(@" mutation OP($data: MySchemaDataInputDto!) { patchMySchemaContent(id: '', data: $data, expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query, Variables = GetInput() }, Permissions.AppContentsUpdateOwn); var expected = new { data = new { patchMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == content.Id && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Data.Equals(content.Data)))) .MustHaveHappened(); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_change_status() { var query = CreateQuery(@" mutation { changeMySchemaContent(id: '', status: 'Published') { id } }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "changeMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_single_content_if_changing_status() { var dueTime = InstantPattern.General.Parse("2021-12-12T11:10:09Z").Value; var query = CreateQuery(@" mutation { changeMySchemaContent(id: '', status: 'Published', dueTime: '2021-12-12T11:10:09Z', expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { data = new { changeMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == contentId && x.DueTime == dueTime && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_changing_status_without_due_time() { var query = CreateQuery(@" mutation { changeMySchemaContent(id: '', status: 'Published', expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { data = new { changeMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == contentId && x.DueTime == null && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published))) .MustHaveHappened(); } [Fact] public async Task Should_return_single_content_if_changing_status_with_null_due_time() { var query = CreateQuery(@" mutation { changeMySchemaContent(id: '', status: 'Published', dueTime: null, expectedVersion: 10) { } }"); commandContext.Complete(content); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsChangeStatusOwn); var expected = new { data = new { changeMySchemaContent = TestContent.Response(content) } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == contentId && x.DueTime == null && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId) && x.Status == Status.Published))) .MustHaveHappened(); } [Fact] public async Task Should_return_error_if_user_has_no_permission_to_delete() { var query = CreateQuery(@" mutation { deleteMySchemaContent(id: '') { version } }"); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsReadOwn); var expected = new { errors = new[] { new { message = "You do not have the necessary permission.", locations = new[] { new { line = 3, column = 19 } }, path = new[] { "deleteMySchemaContent" } } }, data = (object?)null }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync(A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_return_new_version_if_deleting_content() { var query = CreateQuery(@" mutation { deleteMySchemaContent(id: '', expectedVersion: 10) { version } }"); commandContext.Complete(CommandResult.Empty(contentId, 13, 12)); var result = await ExecuteAsync(new ExecutionOptions { Query = query }, Permissions.AppContentsDeleteOwn); var expected = new { data = new { deleteMySchemaContent = new { version = 13 } } }; AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( A.That.Matches(x => x.ContentId == contentId && x.ExpectedVersion == 10 && x.SchemaId.Equals(TestSchemas.DefaultId)))) .MustHaveHappened(); } private string CreateQuery(string query) { query = query .Replace("", contentId.ToString(), StringComparison.Ordinal) .Replace("'", "\"", StringComparison.Ordinal) .Replace("`", "\"", StringComparison.Ordinal) .Replace("", TestContent.AllFields, StringComparison.Ordinal); if (query.Contains("", StringComparison.Ordinal)) { var data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id); var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented); // Use Properties without quotes. dataJson = Regex.Replace(dataJson, "\"([^\"]+)\":", x => x.Groups[1].Value + ":"); // Use pure integer numbers. dataJson = dataJson.Replace(".0", string.Empty, StringComparison.Ordinal); // Use enum values whithout quotes. dataJson = dataJson.Replace("\"EnumA\"", "EnumA", StringComparison.Ordinal); dataJson = dataJson.Replace("\"EnumB\"", "EnumB", StringComparison.Ordinal); query = query.Replace("", dataJson, StringComparison.Ordinal); } return query; } private Inputs GetInput() { var input = new { data = TestContent.Input(content, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id) }; return serializer.ReadNode(JObject.FromObject(input))!; } } }