Browse Source

Merge branch 'release/4.x' of github.com:Squidex/squidex

# Conflicts:
#	backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs
pull/590/head
Sebastian 5 years ago
parent
commit
7eabd7e24f
  1. 31
      backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs
  2. 21
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs
  3. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  4. 17
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  7. 114
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs
  10. 9
      backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs
  11. 14
      backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  12. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  13. 2
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  14. 38
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  15. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs
  16. 48
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs
  17. 44
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  18. 106
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  19. 1
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs
  20. 2
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs
  21. 4
      backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs
  22. 1
      backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs
  23. 18
      backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs
  24. 2
      backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  25. 2
      backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs
  26. 98
      backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs
  27. 4
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  28. 6
      backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs
  29. 2
      backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs
  30. 2
      backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

31
backend/src/Squidex.Domain.Apps.Entities/Contents/BulkUpdateCommandMiddleware.cs

@ -20,17 +20,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class BulkUpdateCommandMiddleware : ICommandMiddleware
{
private readonly IServiceProvider serviceProvider;
private readonly IContentQueryService contentQuery;
private readonly IContextProvider contextProvider;
public BulkUpdateCommandMiddleware(IServiceProvider serviceProvider, IContentQueryService contentQuery, IContextProvider contextProvider)
public BulkUpdateCommandMiddleware(IContentQueryService contentQuery, IContextProvider contextProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(contextProvider, nameof(contextProvider));
this.serviceProvider = serviceProvider;
this.contentQuery = contentQuery;
this.contextProvider = contextProvider;
}
@ -62,23 +59,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
case BulkUpdateType.Upsert:
{
if (id.HasValue)
{
var command = SimpleMapper.Map(bulkUpdates, new UpdateContent { Data = job.Data, ContentId = id.Value });
await context.CommandBus.PublishAsync(command);
var command = SimpleMapper.Map(bulkUpdates, new UpsertContent { Data = job.Data });
results[index] = new BulkUpdateResultItem { ContentId = id };
}
else
if (id != null && id != DomainId.Empty)
{
var command = SimpleMapper.Map(bulkUpdates, new CreateContent { Data = job.Data });
await InsertAsync(command);
command.ContentId = id.Value;
}
result.ContentId = command.ContentId;
}
await context.CommandBus.PublishAsync(command);
break;
}
@ -147,15 +137,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private async Task InsertAsync(CreateContent command)
{
var content = serviceProvider.GetRequiredService<ContentDomainObject>();
content.Setup(command.ContentId);
await content.ExecuteAsync(command);
}
private async Task<DomainId?> FindIdAsync(Context context, string schema, BulkUpdateJob job)
{
var id = job.Id;

21
backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class UpsertContent : ContentDataCommand, ISchemaCommand
{
public bool Publish { get; set; }
public UpsertContent()
{
ContentId = DomainId.NewGuid();
}
}
}

18
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
protected override bool CanAcceptCreation(ICommand command)
{
return command is ContentCommand;
return command is CreateContent;
}
protected override bool CanAccept(ICommand command)
@ -64,6 +64,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
switch (command)
{
case UpsertContent uspertContent:
{
if (Version > EtagVersion.Empty)
{
var updateContent = SimpleMapper.Map(uspertContent, new UpdateContent());
return ExecuteAsync(updateContent);
}
else
{
var createContent = SimpleMapper.Map(uspertContent, new CreateContent());
return ExecuteAsync(createContent);
}
}
case CreateContent createContent:
return CreateReturnAsync(createContent, async c =>
{

17
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs

@ -39,18 +39,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = $"update{schemaType}Content",
Arguments = ContentActions.UpdateOrPatch.Arguments(inputType),
Arguments = ContentActions.Update.Arguments(inputType),
ResolvedType = contentType,
Resolver = ContentActions.UpdateOrPatch.Update(appId, schemaId),
Resolver = ContentActions.Update.Resolver(appId, schemaId),
Description = $"Update an {schemaName} content by id."
});
AddField(new FieldType
{
Name = $"upsert{schemaType}Content",
Arguments = ContentActions.Upsert.Arguments(inputType),
ResolvedType = contentType,
Resolver = ContentActions.Upsert.Resolver(appId, schemaId),
Description = $"Upsert an {schemaName} content by id."
});
AddField(new FieldType
{
Name = $"patch{schemaType}Content",
Arguments = ContentActions.UpdateOrPatch.Arguments(inputType),
Arguments = ContentActions.Patch.Arguments(inputType),
ResolvedType = contentType,
Resolver = ContentActions.UpdateOrPatch.Patch(appId, schemaId),
Resolver = ContentActions.Patch.Resolver(appId, schemaId),
Description = $"Patch an {schemaName} content by id."
});

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the asset (GUID).",
Description = "The id of the asset (usually GUID).",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullDomainId
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullGuid,
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the asset."
});

114
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (GUID).",
Description = "The id of the content (usually GUID).",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullDomainId
}
@ -160,6 +160,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "Set to true to autopublish content.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
}
@ -170,13 +177,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var contentPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
var contentId = c.GetArgument<string?>("id");
return new CreateContent { Data = contentData, Publish = contentPublish };
var command = new CreateContent { Data = contentData, Publish = contentPublish };
if (!string.IsNullOrWhiteSpace(contentId))
{
command.ContentId = contentId;
}
return command;
});
}
}
public static class UpdateOrPatch
public static class Upsert
{
public static QueryArguments Arguments(IGraphType inputType)
{
@ -187,7 +202,57 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "id",
Description = "The id of the content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = "The data for the content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
{
return ResolveAsync<IEnrichedContentEntity>(appId, schemaId, c =>
{
var contentPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
var contentId = c.GetArgument<string>("id");
return new UpsertContent { ContentId = contentId, Data = contentData, Publish = contentPublish };
});
}
}
public static class Update
{
public static QueryArguments Arguments(IGraphType inputType)
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None)
{
@ -206,7 +271,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
};
}
public static IFieldResolver Update(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
{
return ResolveAsync<IEnrichedContentEntity>(appId, schemaId, c =>
{
@ -216,8 +281,39 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return new UpdateContent { ContentId = contentId, Data = contentData };
});
}
}
public static class Patch
{
public static QueryArguments Arguments(IGraphType inputType)
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = "The data for the content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
public static IFieldResolver Patch(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
{
return ResolveAsync<IEnrichedContentEntity>(appId, schemaId, c =>
{
@ -236,9 +332,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (GUID)",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None)
{
@ -283,7 +379,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (GUID)",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullGuid,
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = $"The id of the {schemaName} content."
});

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullGuid,
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the content."
});

9
backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs

@ -22,6 +22,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
private readonly IEventSubscription eventSubscription;
private readonly IDataflowBlock pipelineEnd;
public object Sender
{
get { return eventSubscription.Sender!; }
}
private sealed class Job
{
public StoredEvent? StoredEvent { get; set; }
@ -91,11 +96,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
if (exception != null)
{
await grain.OnErrorAsync(exception);
await grain.OnErrorAsync(Sender, exception);
}
else
{
await grain.OnEventsAsync(GetEvents(jobsBySender), GetPosition(jobsBySender));
await grain.OnEventsAsync(Sender, GetEvents(jobsBySender), GetPosition(jobsBySender));
}
}
}

14
backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -81,8 +81,13 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
return State.ToInfo(eventConsumer!.Name).AsImmutable();
}
public Task OnEventsAsync(IReadOnlyList<Envelope<IEvent>> events, string position)
public Task OnEventsAsync(object sender, IReadOnlyList<Envelope<IEvent>> events, string position)
{
if (!ReferenceEquals(sender, currentSubscriber?.Sender))
{
return Task.CompletedTask;
}
return DoAndUpdateStateAsync(async () =>
{
await DispatchAsync(events);
@ -91,8 +96,13 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
});
}
public Task OnErrorAsync(Exception exception)
public Task OnErrorAsync(object sender, Exception exception)
{
if (!ReferenceEquals(sender, currentSubscriber?.Sender))
{
return Task.CompletedTask;
}
return DoAndUpdateStateAsync(() =>
{
Unsubscribe();

2
backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -162,7 +162,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
}
/// <summary>
/// Get the app image.
/// Upload the app image.
/// </summary>
/// <param name="app">The name of the app to update.</param>
/// <param name="file">The file to upload.</param>

2
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -71,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ApiPermission]
[ApiCosts(0.5)]
[AllowAnonymous]
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, string more, [FromQuery] AssetContentQueryDto queries)
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, [FromQuery] AssetContentQueryDto queries, string? more = null)
{
IAssetEntity? asset;

38
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -319,6 +319,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="name">The name of the schema.</param>
/// <param name="request">The full data for the content item.</param>
/// <param name="publish">True to automatically publish the content.</param>
/// <param name="id">The optional custom content id.</param>
/// <returns>
/// 201 => Content created.
/// 404 => Content, schema or app not found.
@ -332,10 +333,15 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ProducesResponseType(typeof(ContentsDto), 201)]
[ApiPermissionOrAnonymous(Permissions.AppContentsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
public async Task<IActionResult> PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false, [FromQuery] string? id = null)
{
var command = new CreateContent { Data = request.ToCleaned(), Publish = publish };
if (!string.IsNullOrWhiteSpace(id))
{
command.ContentId = id;
}
var response = await InvokeCommandAsync(command);
return CreatedAtAction(nameof(GetContent), new { app, name, id = command.ContentId }, response);
@ -403,6 +409,36 @@ namespace Squidex.Areas.Api.Controllers.Contents
return Ok(response);
}
/// <summary>
/// Upsert a content item.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to update.</param>
/// <param name="publish">True to automatically publish the content.</param>
/// <param name="request">The full data for the content item.</param>
/// <returns>
/// 200 => Content updated.
/// 404 => Content references, schema or app not found.
/// 400 => Content data is not valid.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs.
/// </remarks>
[HttpPost]
[Route("content/{app}/{name}/{id}/")]
[ProducesResponseType(typeof(ContentsDto), 200)]
[ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostContent(string app, string name, string id, [FromBody] NamedContentData request, [FromQuery] bool publish = false)
{
var command = new UpsertContent { ContentId = id, Data = request.ToCleaned(), Publish = publish };
var response = await InvokeCommandAsync(command);
return Ok(response);
}
/// <summary>
/// Update a content item.
/// </summary>

1
backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaOpenApiGenerator.cs

@ -125,6 +125,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
operation.AddBody("data", dataSchema, NSwagHelper.SchemaBodyDocs);
operation.AddQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.");
operation.AddQuery("id", JsonObjectType.String, "The optional custom content id.");
operation.AddResponse("201", $"{schemaName} content created.", contentSchema);
operation.AddResponse("400", $"{schemaName} content not valid.");

48
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BulkUpdateCommandMiddlewareTests.cs

@ -22,7 +22,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public class BulkUpdateCommandMiddlewareTests
{
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly ICommandBus commandBus = A.Dummy<ICommandBus>();
@ -35,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contextProvider.Context)
.Returns(requestContext);
sut = new BulkUpdateCommandMiddleware(serviceProvider, contentQuery, contextProvider);
sut = new BulkUpdateCommandMiddleware(contentQuery, contextProvider);
}
[Fact]
@ -48,9 +47,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
await sut.HandleAsync(context);
Assert.True(context.PlainResult is BulkUpdateResult);
A.CallTo(() => serviceProvider.GetService(A<Type>._))
.MustNotHaveHappened();
}
[Fact]
@ -63,21 +59,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
await sut.HandleAsync(context);
Assert.True(context.PlainResult is BulkUpdateResult);
A.CallTo(() => serviceProvider.GetService(A<Type>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_import_contents_when_no_query_defined()
public async Task Should_upsert_content_with_random_id_if_no_query_and_id_defined()
{
var (_, data, _) = CreateTestData(false);
var domainObject = A.Fake<ContentDomainObject>();
A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject)))
.Returns(domainObject);
var command = new BulkUpdateContents
{
Jobs = new List<BulkUpdateJob>
@ -100,23 +88,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(result);
Assert.Equal(1, result.Count(x => x.ContentId != default && x.Exception == null));
A.CallTo(() => domainObject.ExecuteAsync(A<CreateContent>.That.Matches(x => x.Data == data)))
.MustHaveHappenedOnceExactly();
A.CallTo(() => domainObject.Setup(A<DomainId>._))
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId.ToString().Length == 36)))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_import_contents_when_query_returns_no_result()
public async Task Should_upsert_content_with_random_id_if_query_returns_no_result()
{
var (_, data, query) = CreateTestData(false);
var domainObject = A.Fake<ContentDomainObject>();
A.CallTo(() => serviceProvider.GetService(typeof(ContentDomainObject)))
.Returns(domainObject);
var command = new BulkUpdateContents
{
Jobs = new List<BulkUpdateJob>
@ -140,15 +121,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(result);
Assert.Equal(1, result.Count(x => x.ContentId != default && x.Exception == null));
A.CallTo(() => domainObject.ExecuteAsync(A<CreateContent>.That.Matches(x => x.Data == data)))
.MustHaveHappenedOnceExactly();
A.CallTo(() => domainObject.Setup(A<DomainId>._))
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId.ToString().Length == 36)))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_update_content_when_id_defined()
public async Task Should_upsert_content_when_id_defined()
{
var (id, data, _) = CreateTestData(false);
@ -175,12 +154,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(result);
Assert.Equal(1, result.Count(x => x.ContentId != default && x.Exception == null));
A.CallTo(() => commandBus.PublishAsync(A<UpdateContent>.That.Matches(x => x.ContentId == id && x.Data == data)))
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id)))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_update_content_when_query_defined()
public async Task Should_upsert_content_with_custom_id()
{
var (id, data, query) = CreateTestData(true);
@ -210,7 +190,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(result);
Assert.Equal(1, result.Count(x => x.ContentId != default && x.Exception == null));
A.CallTo(() => commandBus.PublishAsync(A<UpdateContent>.That.Matches(x => x.ContentId == id && x.Data == data)))
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x => x.Data == data && x.ContentId == id)))
.MustHaveHappenedOnceExactly();
}
@ -338,7 +319,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Single(result);
Assert.Equal(1, result.Count(x => x.ContentId == id));
A.CallTo(() => commandBus.PublishAsync(A<DeleteContent>.That.Matches(x => x.ContentId == id)))
A.CallTo(() => commandBus.PublishAsync(
A<DeleteContent>.That.Matches(x => x.ContentId == id)))
.MustHaveHappened();
}

44
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -173,6 +173,50 @@ namespace Squidex.Domain.Apps.Entities.Contents
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(CreateContentCommand(command)));
}
[Fact]
public async Task Upsert_should_create_contnet_when_not_found()
{
var command = new UpsertContent { Data = data };
var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Same(data, sut.Snapshot.CurrentVersion.Data);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data, Status = Status.Draft })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(data, null, Status.Draft), "<create-script>", ScriptOptions()))
.MustHaveHappened();
A.CallTo(() => scriptEngine.ExecuteAsync(A<ScriptVars>._, "<change-script>", ScriptOptions()))
.MustNotHaveHappened();
}
[Fact]
public async Task Upsert_should_update_contnet_when_found()
{
var command = new UpsertContent { Data = otherData };
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(otherData, sut.Snapshot.CurrentVersion.Data);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.TransformAsync(ScriptContext(otherData, data, Status.Draft), "<update-script>", ScriptOptions()))
.MustHaveHappened();
}
[Fact]
public async Task Update_should_create_events_and_update_data()
{

106
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var query = @"
mutation {
createMySchemaContent(data: <DATA>) {
createMySchemaContent(data: <DATA>, publish: true) {
<FIELDS>
}
}".Replace("<DATA>", GetDataString()).Replace("<FIELDS>", TestContent.AllFields);
@ -64,6 +64,41 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A<CreateContent>.That.Matches(x =>
x.SchemaId.Equals(schemaId) &&
x.ExpectedVersion == EtagVersion.Any &&
x.Publish &&
x.Data.Equals(content.Data))))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_creating_content_with_custom_id()
{
var query = @"
mutation {
createMySchemaContent(data: <DATA>, id: ""123"", publish: true) {
<FIELDS>
}
}".Replace("<DATA>", GetDataString()).Replace("<FIELDS>", TestContent.AllFields);
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
createMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<CreateContent>.That.Matches(x =>
x.SchemaId.Equals(schemaId) &&
x.ExpectedVersion == EtagVersion.Any &&
x.ContentId == DomainId.Create("123") &&
x.Publish &&
x.Data.Equals(content.Data))))
.MustHaveHappened();
}
@ -73,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var query = @"
mutation OP($data: MySchemaDataInputDto!) {
createMySchemaContent(data: $data) {
createMySchemaContent(data: $data, publish: true) {
<FIELDS>
}
}".Replace("<FIELDS>", TestContent.AllFields);
@ -96,6 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A<CreateContent>.That.Matches(x =>
x.SchemaId.Equals(schemaId) &&
x.ExpectedVersion == EtagVersion.Any &&
x.Publish &&
x.Data.Equals(content.Data))))
.MustHaveHappened();
}
@ -164,6 +200,72 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_upserting_content()
{
var query = @"
mutation {
upsertMySchemaContent(id: ""<ID>"", data: <DATA>, publish: true, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<DATA>", GetDataString()).Replace("<FIELDS>", TestContent.AllFields);
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
upsertMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ExpectedVersion == 10 &&
x.Publish &&
x.Data.Equals(content.Data))))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_upserting_content_with_variable()
{
var query = @"
mutation OP($data: MySchemaDataInputDto!) {
upsertMySchemaContent(id: ""<ID>"", data: $data, publish: true, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<FIELDS>", TestContent.AllFields);
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() });
var expected = new
{
data = new
{
upsertMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<UpsertContent>.That.Matches(x =>
x.ContentId == content.Id &&
x.ExpectedVersion == 10 &&
x.Publish &&
x.Data.Equals(content.Data))))
.MustHaveHappened();
}
[Fact]
public async Task Should_return_single_content_when_patching_content()
{

1
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs

@ -18,7 +18,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text
public class CachingTextIndexerStateTests
{
private readonly ITextIndexerState inner = A.Fake<ITextIndexerState>();
private readonly DomainId appId = DomainId.NewGuid();
private readonly DomainId contentId = DomainId.NewGuid();
private readonly CachingTextIndexerState sut;

2
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs

@ -27,8 +27,6 @@ namespace Squidex.Infrastructure.EventSourcing
mongoClient = new MongoClient(connectionString);
mongoDatabase = mongoClient.GetDatabase($"EventStoreTest");
Cleanup();
BsonJsonConvention.Register(JsonSerializer.Create(JsonHelper.DefaultSettings()));
EventStore = new MongoEventStore(mongoDatabase, notifier);

4
backend/tools/TestSuite/TestSuite.ApiTests/AppTests.cs

@ -287,7 +287,7 @@ namespace TestSuite.ApiTests
// STEP 2: Update pattern.
var updateRequest = new UpdatePatternDto { Name = patternName, Pattern = patternRegex2 };
var patterns_2 = await _.Apps.PutPatternAsync(_.AppName, pattern_1.Id.ToString(), updateRequest);
var patterns_2 = await _.Apps.PutPatternAsync(_.AppName, pattern_1.Id, updateRequest);
var pattern_2 = patterns_2.Items.Single(x => x.Name == patternName);
// Should return pattern with correct regex.
@ -295,7 +295,7 @@ namespace TestSuite.ApiTests
// STEP 3: Remove pattern.
var patterns_3 = await _.Apps.DeletePatternAsync(_.AppName, pattern_2.Id.ToString());
var patterns_3 = await _.Apps.DeletePatternAsync(_.AppName, pattern_2.Id);
// Should not return deleted pattern.
Assert.DoesNotContain(patterns_3.Items, x => x.Id == pattern_2.Id);

1
backend/tools/TestSuite/TestSuite.ApiTests/AssetFormatTests.cs

@ -13,6 +13,7 @@ using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
#pragma warning disable SA1507 // Code should not contain multiple blank lines in a row
#pragma warning disable CS0612 // Type or member is obsolete
namespace TestSuite.ApiTests
{

18
backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs

@ -65,7 +65,7 @@ namespace TestSuite.ApiTests
}
};
var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id.ToString(), metadataRequest);
var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, metadataRequest);
// Should provide metadata.
Assert.Equal(metadataRequest.Metadata, asset_2.Metadata);
@ -74,7 +74,7 @@ namespace TestSuite.ApiTests
// STEP 3: Annotate slug.
var slugRequest = new AnnotateAssetDto { Slug = "my-image" };
var asset_3 = await _.Assets.PutAssetAsync(_.AppName, asset_2.Id.ToString(), slugRequest);
var asset_3 = await _.Assets.PutAssetAsync(_.AppName, asset_2.Id, slugRequest);
// Should provide updated slug.
Assert.Equal(slugRequest.Slug, asset_3.Slug);
@ -83,7 +83,7 @@ namespace TestSuite.ApiTests
// STEP 3: Annotate file name.
var fileNameRequest = new AnnotateAssetDto { FileName = "My Image" };
var asset_4 = await _.Assets.PutAssetAsync(_.AppName, asset_3.Id.ToString(), fileNameRequest);
var asset_4 = await _.Assets.PutAssetAsync(_.AppName, asset_3.Id, fileNameRequest);
// Should provide updated file name.
Assert.Equal(fileNameRequest.FileName, asset_4.FileName);
@ -111,7 +111,7 @@ namespace TestSuite.ApiTests
// STEP 4: Protect asset
var protectRequest = new AnnotateAssetDto { IsProtected = true };
var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id.ToString(), protectRequest);
var asset_2 = await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, protectRequest);
// STEP 5: Download asset with authentication.
@ -119,7 +119,7 @@ namespace TestSuite.ApiTests
{
var downloaded = new MemoryStream();
using (var assetStream = await _.Assets.GetAssetContentAsync(asset_2.Id.ToString()))
using (var assetStream = await _.Assets.GetAssetContentBySlugAsync(_.AppName, asset_2.Id))
{
await assetStream.Stream.CopyToAsync(downloaded);
}
@ -147,10 +147,10 @@ namespace TestSuite.ApiTests
// STEP 2: Delete asset
await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id.ToString());
await _.Assets.DeleteAssetAsync(_.AppName, asset_1.Id);
// Should return 404 when asset deleted.
var ex = await Assert.ThrowsAsync<SquidexManagementException>(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id.ToString()));
var ex = await Assert.ThrowsAsync<SquidexManagementException>(() => _.Assets.GetAssetAsync(_.AppName, asset_1.Id));
Assert.Equal(404, ex.StatusCode);
}
@ -174,7 +174,7 @@ namespace TestSuite.ApiTests
// STEP 3: Add custom metadata.
asset_1.Metadata["custom"] = "foo";
await _.Assets.PutAssetAsync(_.AppName, asset_1.Id.ToString(), new AnnotateAssetDto
await _.Assets.PutAssetAsync(_.AppName, asset_1.Id, new AnnotateAssetDto
{
Metadata = asset_1.Metadata
});
@ -209,7 +209,7 @@ namespace TestSuite.ApiTests
// STEP 4: Delete folder.
await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id.ToString());
await _.Assets.DeleteAssetFolderAsync(_.AppName, folder_1.Id);
// STEP 5: Wait for recursive deleter to delete the asset.

2
backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -35,7 +35,7 @@ namespace TestSuite.ApiTests
{
var items = await _.Contents.GetAsync(new ContentQuery { OrderBy = "data/number/iv asc" });
var itemsById = await _.Contents.GetAsync(new HashSet<Guid>(items.Items.Take(3).Select(x => x.Id)));
var itemsById = await _.Contents.GetAsync(new HashSet<string>(items.Items.Take(3).Select(x => x.Id)));
Assert.Equal(3, itemsById.Items.Count);
Assert.Equal(3, itemsById.Total);

2
backend/tools/TestSuite/TestSuite.ApiTests/ContentReferencesTests.cs

@ -55,7 +55,7 @@ namespace TestSuite.ApiTests
// STEP 5: Query new item again
var contentB_3 = await _.Contents.GetAsync(contentB_1.Id);
Assert.Equal(new Guid[] { contentA_1.Id }, contentB_3.Data.References);
Assert.Equal(new string[] { contentA_1.Id }, contentB_3.Data.References);
}
}
}

98
backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.ClientLibrary;
using TestSuite.Fixtures;
@ -25,6 +26,69 @@ namespace TestSuite.ApiTests
_ = fixture;
}
[Fact]
public async Task Should_return_item_published_item()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.GetAsync(content.Id);
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_not_return_archived_item()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true);
await _.Contents.ChangeStatusAsync(content.Id, Status.Archived);
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_not_return_unpublished_item()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.ChangeStatusAsync(content.Id, Status.Draft);
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
}
finally
{
if (content != null)
{
await _.Contents.DeleteAsync(content.Id);
}
}
}
[Fact]
public async Task Should_create_strange_text()
{
@ -49,7 +113,7 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_not_return_not_published_item()
public async Task Should_create_non_published_item()
{
TestEntity content = null;
try
@ -68,7 +132,7 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_return_item_published_with_creation()
public async Task Should_create_published_item()
{
TestEntity content = null;
try
@ -87,15 +151,16 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_return_item_published_item()
public async Task Should_create_item_with_custom_id()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
var id = Guid.NewGuid().ToString();
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.GetAsync(content.Id);
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, id, true);
Assert.Equal(id, content.Id);
}
finally
{
@ -107,16 +172,16 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_not_return_archived_item()
public async Task Should_create_item_with_custom_id_and_upsert()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 }, true);
var id = Guid.NewGuid().ToString();
await _.Contents.ChangeStatusAsync(content.Id, Status.Archived);
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, true);
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
Assert.Equal(id, content.Id);
}
finally
{
@ -128,17 +193,20 @@ namespace TestSuite.ApiTests
}
[Fact]
public async Task Should_not_return_unpublished_item()
public async Task Should_upsert_item()
{
TestEntity content = null;
try
{
content = await _.Contents.CreateAsync(new TestEntityData { Number = 1 });
var id = Guid.NewGuid().ToString();
await _.Contents.ChangeStatusAsync(content.Id, Status.Published);
await _.Contents.ChangeStatusAsync(content.Id, Status.Draft);
content = await _.Contents.UpsertAsync(id, new TestEntityData { Number = 1 }, true);
await Assert.ThrowsAsync<SquidexException>(() => _.Contents.GetAsync(content.Id));
await _.Contents.UpsertAsync(id, new TestEntityData { Number = 2 });
var updated = await _.Contents.GetAsync(content.Id);
Assert.Equal(2, updated.Data.Number);
}
finally
{

4
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -148,7 +148,7 @@ namespace TestSuite.ApiTests
Name = "cities",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<Guid> { cities.Id }
SchemaIds = new List<string> { cities.Id }
}
}
},
@ -174,7 +174,7 @@ namespace TestSuite.ApiTests
Name = "states",
Properties = new ReferencesFieldPropertiesDto
{
SchemaIds = new List<Guid> { states.Id }
SchemaIds = new List<string> { states.Id }
}
}
},

6
backend/tools/TestSuite/TestSuite.Shared/Fixtures/AssetFixture.cs

@ -46,11 +46,11 @@ namespace TestSuite.Fixtures
{
var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), asset.MimeType);
return await Assets.PutAssetContentAsync(AppName, asset.Id.ToString(), upload);
return await Assets.PutAssetContentAsync(AppName, asset.Id, upload);
}
}
public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null, Guid? parentId = null)
public async Task<AssetDto> UploadFileAsync(string path, string mimeType, string fileName = null, string parentId = null)
{
var fileInfo = new FileInfo(path);
@ -58,7 +58,7 @@ namespace TestSuite.Fixtures
{
var upload = new FileParameter(stream, fileName ?? RandomName(fileInfo), mimeType);
return await Assets.PostAssetAsync(AppName, upload, parentId);
return await Assets.PostAssetAsync(AppName, parentId?.ToString(), upload);
}
}

2
backend/tools/TestSuite/TestSuite.Shared/Model/TestEntityWithReferences.cs

@ -42,6 +42,6 @@ namespace TestSuite.Model
public sealed class TestEntityWithReferencesData
{
[JsonConverter(typeof(InvariantConverter))]
public Guid[] References { get; set; }
public string[] References { get; set; }
}
}

2
backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj

@ -9,7 +9,7 @@
</PackageReference>
<PackageReference Include="Lazy.Fody" Version="1.8.0" PrivateAssets="all" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.3.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.5.0-beta2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.4.1" />
</ItemGroup>

Loading…
Cancel
Save