mirror of https://github.com/Squidex/squidex.git
50 changed files with 4944 additions and 9 deletions
@ -0,0 +1,30 @@ |
|||
// ==========================================================================
|
|||
// CachingProviderBase.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities |
|||
{ |
|||
public abstract class CachingProviderBase |
|||
{ |
|||
private readonly IMemoryCache cache; |
|||
|
|||
protected IMemoryCache Cache |
|||
{ |
|||
get { return cache; } |
|||
} |
|||
|
|||
protected CachingProviderBase(IMemoryCache cache) |
|||
{ |
|||
Guard.NotNull(cache, nameof(cache)); |
|||
|
|||
this.cache = cache; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
// ==========================================================================
|
|||
// ContentHistoryEventsCreator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.History; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentHistoryEventsCreator : HistoryEventsCreatorBase |
|||
{ |
|||
public ContentHistoryEventsCreator(TypeNameRegistry typeNameRegistry) |
|||
: base(typeNameRegistry) |
|||
{ |
|||
AddEventMessage<ContentCreated>( |
|||
"created content item."); |
|||
|
|||
AddEventMessage<ContentUpdated>( |
|||
"updated content item."); |
|||
|
|||
AddEventMessage<ContentDeleted>( |
|||
"deleted content item."); |
|||
|
|||
AddEventMessage<ContentStatusChanged>( |
|||
"changed status of content item to {[Status]}."); |
|||
} |
|||
|
|||
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) |
|||
{ |
|||
var channel = $"contents.{@event.Headers.AggregateId()}"; |
|||
|
|||
var result = ForEvent(@event.Payload, channel); |
|||
|
|||
if (@event.Payload is ContentStatusChanged contentStatusChanged) |
|||
{ |
|||
result = result.AddParameter("Status", contentStatusChanged.Status); |
|||
} |
|||
|
|||
return Task.FromResult(result); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,216 @@ |
|||
// ==========================================================================
|
|||
// ContentQueryService.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.OData; |
|||
using Microsoft.OData.UriParser; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Infrastructure.Security; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentQueryService : IContentQueryService |
|||
{ |
|||
private readonly IContentRepository contentRepository; |
|||
private readonly IAppProvider appProvider; |
|||
private readonly IScriptEngine scriptEngine; |
|||
private readonly EdmModelBuilder modelBuilder; |
|||
|
|||
public ContentQueryService( |
|||
IContentRepository contentRepository, |
|||
IAppProvider appProvider, |
|||
IScriptEngine scriptEngine, |
|||
EdmModelBuilder modelBuilder) |
|||
{ |
|||
Guard.NotNull(contentRepository, nameof(contentRepository)); |
|||
Guard.NotNull(scriptEngine, nameof(scriptEngine)); |
|||
Guard.NotNull(modelBuilder, nameof(modelBuilder)); |
|||
Guard.NotNull(appProvider, nameof(appProvider)); |
|||
|
|||
this.contentRepository = contentRepository; |
|||
this.appProvider = appProvider; |
|||
this.scriptEngine = scriptEngine; |
|||
this.modelBuilder = modelBuilder; |
|||
} |
|||
|
|||
public async Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id) |
|||
{ |
|||
Guard.NotNull(app, nameof(app)); |
|||
Guard.NotNull(user, nameof(user)); |
|||
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
|||
|
|||
var isFrontendClient = user.IsInClient("squidex-frontend"); |
|||
|
|||
var schema = await FindSchemaAsync(app, schemaIdOrName); |
|||
|
|||
var content = await contentRepository.FindContentAsync(app, schema, id); |
|||
|
|||
if (content == null || (content.Status != Status.Published && !isFrontendClient)) |
|||
{ |
|||
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); |
|||
} |
|||
|
|||
content = TransformContent(user, schema, new List<IContentEntity> { content })[0]; |
|||
|
|||
return (schema, content); |
|||
} |
|||
|
|||
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query) |
|||
{ |
|||
Guard.NotNull(app, nameof(app)); |
|||
Guard.NotNull(user, nameof(user)); |
|||
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
|||
|
|||
var schema = await FindSchemaAsync(app, schemaIdOrName); |
|||
|
|||
var parsedQuery = ParseQuery(app, query, schema); |
|||
|
|||
var status = ParseStatus(user, archived); |
|||
|
|||
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), parsedQuery); |
|||
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), parsedQuery); |
|||
|
|||
await Task.WhenAll(taskForItems, taskForCount); |
|||
|
|||
var list = TransformContent(user, schema, taskForItems.Result.ToList()); |
|||
|
|||
return (schema, taskForCount.Result, list); |
|||
} |
|||
|
|||
public async Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
Guard.NotNull(app, nameof(app)); |
|||
Guard.NotNull(user, nameof(user)); |
|||
Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); |
|||
|
|||
var schema = await FindSchemaAsync(app, schemaIdOrName); |
|||
|
|||
var status = ParseStatus(user, archived); |
|||
|
|||
var taskForItems = contentRepository.QueryAsync(app, schema, status.ToArray(), ids); |
|||
var taskForCount = contentRepository.CountAsync(app, schema, status.ToArray(), ids); |
|||
|
|||
await Task.WhenAll(taskForItems, taskForCount); |
|||
|
|||
var list = TransformContent(user, schema, taskForItems.Result.ToList()); |
|||
|
|||
return (schema, taskForCount.Result, list); |
|||
} |
|||
|
|||
private List<IContentEntity> TransformContent(ClaimsPrincipal user, ISchemaEntity schema, List<IContentEntity> contents) |
|||
{ |
|||
var scriptText = schema.ScriptQuery; |
|||
|
|||
if (!string.IsNullOrWhiteSpace(scriptText)) |
|||
{ |
|||
for (var i = 0; i < contents.Count; i++) |
|||
{ |
|||
var content = contents[i]; |
|||
var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText); |
|||
|
|||
contents[i] = SimpleMapper.Map(content, new Content { Data = contentData }); |
|||
} |
|||
} |
|||
|
|||
return contents; |
|||
} |
|||
|
|||
private ODataUriParser ParseQuery(IAppEntity app, string query, ISchemaEntity schema) |
|||
{ |
|||
try |
|||
{ |
|||
var model = modelBuilder.BuildEdmModel(schema, app); |
|||
|
|||
return model.ParseQuery(query); |
|||
} |
|||
catch (ODataException ex) |
|||
{ |
|||
throw new ValidationException($"Failed to parse query: {ex.Message}", ex); |
|||
} |
|||
} |
|||
|
|||
public async Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName) |
|||
{ |
|||
Guard.NotNull(app, nameof(app)); |
|||
|
|||
ISchemaEntity schema = null; |
|||
|
|||
if (Guid.TryParse(schemaIdOrName, out var id)) |
|||
{ |
|||
schema = await appProvider.GetSchemaAsync(app.Name, id); |
|||
} |
|||
|
|||
if (schema == null) |
|||
{ |
|||
schema = await appProvider.GetSchemaAsync(app.Name, schemaIdOrName); |
|||
} |
|||
|
|||
if (schema == null) |
|||
{ |
|||
throw new DomainObjectNotFoundException(schemaIdOrName, typeof(ISchemaEntity)); |
|||
} |
|||
|
|||
return schema; |
|||
} |
|||
|
|||
private static List<Status> ParseStatus(ClaimsPrincipal user, bool archived) |
|||
{ |
|||
var status = new List<Status>(); |
|||
|
|||
if (user.IsInClient("squidex-frontend")) |
|||
{ |
|||
if (archived) |
|||
{ |
|||
status.Add(Status.Archived); |
|||
} |
|||
else |
|||
{ |
|||
status.Add(Status.Draft); |
|||
status.Add(Status.Published); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
status.Add(Status.Published); |
|||
} |
|||
|
|||
return status; |
|||
} |
|||
|
|||
private sealed class Content : IContentEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
public Guid AppId { get; set; } |
|||
|
|||
public long Version { get; set; } |
|||
|
|||
public Instant Created { get; set; } |
|||
public Instant LastModified { get; set; } |
|||
|
|||
public RefToken CreatedBy { get; set; } |
|||
public RefToken LastModifiedBy { get; set; } |
|||
|
|||
public NamedContentData Data { get; set; } |
|||
|
|||
public Status Status { get; set; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// ==========================================================================
|
|||
// EdmModelBuilder.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.OData.Edm; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.GenerateEdmSchema; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Edm |
|||
{ |
|||
public class EdmModelBuilder : CachingProviderBase |
|||
{ |
|||
public EdmModelBuilder(IMemoryCache cache) |
|||
: base(cache) |
|||
{ |
|||
} |
|||
|
|||
public virtual IEdmModel BuildEdmModel(ISchemaEntity schema, IAppEntity app) |
|||
{ |
|||
Guard.NotNull(schema, nameof(schema)); |
|||
|
|||
var cacheKey = $"{schema.Id}_{schema.Version}_{app.Id}_{app.Version}"; |
|||
|
|||
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry => |
|||
{ |
|||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); |
|||
|
|||
return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); |
|||
}); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
private static EdmModel BuildEdmModel(Schema schema, PartitionResolver partitionResolver) |
|||
{ |
|||
var model = new EdmModel(); |
|||
|
|||
var container = new EdmEntityContainer("Squidex", "Container"); |
|||
|
|||
var schemaType = schema.BuildEdmType(partitionResolver, x => |
|||
{ |
|||
model.AddElement(x); |
|||
|
|||
return x; |
|||
}); |
|||
|
|||
var entityType = new EdmEntityType("Squidex", schema.Name); |
|||
entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false)); |
|||
entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32); |
|||
entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset); |
|||
entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String); |
|||
entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset); |
|||
entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String); |
|||
|
|||
model.AddElement(container); |
|||
model.AddElement(schemaType); |
|||
model.AddElement(entityType); |
|||
|
|||
container.AddEntitySet("ContentSet", entityType); |
|||
|
|||
return model; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// ==========================================================================
|
|||
// EdmModelExtensions.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using Microsoft.OData.Edm; |
|||
using Microsoft.OData.UriParser; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Edm |
|||
{ |
|||
public static class EdmModelExtensions |
|||
{ |
|||
public static ODataUriParser ParseQuery(this IEdmModel model, string query) |
|||
{ |
|||
if (!model.EntityContainer.EntitySets().Any()) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
query = query ?? string.Empty; |
|||
|
|||
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); |
|||
|
|||
if (query.StartsWith("?", StringComparison.Ordinal)) |
|||
{ |
|||
query = query.Substring(1); |
|||
} |
|||
|
|||
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); |
|||
|
|||
return parser; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
// ==========================================================================
|
|||
// CachingGraphQLService.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService |
|||
{ |
|||
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); |
|||
private readonly IContentQueryService contentQuery; |
|||
private readonly IGraphQLUrlGenerator urlGenerator; |
|||
private readonly IAssetRepository assetRepository; |
|||
private readonly IAppProvider appProvider; |
|||
|
|||
public CachingGraphQLService(IMemoryCache cache, |
|||
IAppProvider appProvider, |
|||
IAssetRepository assetRepository, |
|||
IContentQueryService contentQuery, |
|||
IGraphQLUrlGenerator urlGenerator) |
|||
: base(cache) |
|||
{ |
|||
Guard.NotNull(appProvider, nameof(appProvider)); |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
Guard.NotNull(contentQuery, nameof(urlGenerator)); |
|||
Guard.NotNull(contentQuery, nameof(contentQuery)); |
|||
|
|||
this.appProvider = appProvider; |
|||
this.assetRepository = assetRepository; |
|||
this.contentQuery = contentQuery; |
|||
this.urlGenerator = urlGenerator; |
|||
} |
|||
|
|||
public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query) |
|||
{ |
|||
Guard.NotNull(app, nameof(app)); |
|||
Guard.NotNull(query, nameof(query)); |
|||
|
|||
if (string.IsNullOrWhiteSpace(query.Query)) |
|||
{ |
|||
return (new object(), new object[0]); |
|||
} |
|||
|
|||
var modelContext = await GetModelAsync(app); |
|||
var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator); |
|||
|
|||
return await modelContext.ExecuteAsync(queryContext, query); |
|||
} |
|||
|
|||
private async Task<GraphQLModel> GetModelAsync(IAppEntity app) |
|||
{ |
|||
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); |
|||
|
|||
var modelContext = Cache.Get<GraphQLModel>(cacheKey); |
|||
|
|||
if (modelContext == null) |
|||
{ |
|||
var allSchemas = await appProvider.GetSchemasAsync(app.Name); |
|||
|
|||
modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator); |
|||
|
|||
Cache.Set(cacheKey, modelContext, CacheDuration); |
|||
} |
|||
|
|||
return modelContext; |
|||
} |
|||
|
|||
private static object CreateCacheKey(Guid appId, string etag) |
|||
{ |
|||
return $"GraphQLModel_{appId}_{etag}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,229 @@ |
|||
// ==========================================================================
|
|||
// GraphQLContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using GraphQL; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using GraphQLSchema = GraphQL.Types.Schema; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public sealed class GraphQLModel : IGraphQLContext |
|||
{ |
|||
private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos; |
|||
private readonly Dictionary<Guid, ContentGraphType> schemaTypes = new Dictionary<Guid, ContentGraphType>(); |
|||
private readonly Dictionary<Guid, ISchemaEntity> schemas; |
|||
private readonly PartitionResolver partitionResolver; |
|||
private readonly IAppEntity app; |
|||
private readonly IGraphType assetType; |
|||
private readonly IGraphType assetListType; |
|||
private readonly GraphQLSchema graphQLSchema; |
|||
|
|||
public bool CanGenerateAssetSourceUrl { get; } |
|||
|
|||
public GraphQLModel(IAppEntity app, IEnumerable<ISchemaEntity> schemas, IGraphQLUrlGenerator urlGenerator) |
|||
{ |
|||
this.app = app; |
|||
|
|||
CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; |
|||
|
|||
partitionResolver = app.PartitionResolver(); |
|||
|
|||
assetType = new AssetGraphType(this); |
|||
assetListType = new ListGraphType(new NonNullGraphType(assetType)); |
|||
|
|||
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> |
|||
{ |
|||
{ |
|||
typeof(StringField), |
|||
field => ResolveDefault("String") |
|||
}, |
|||
{ |
|||
typeof(BooleanField), |
|||
field => ResolveDefault("Boolean") |
|||
}, |
|||
{ |
|||
typeof(NumberField), |
|||
field => ResolveDefault("Float") |
|||
}, |
|||
{ |
|||
typeof(DateTimeField), |
|||
field => ResolveDefault("Date") |
|||
}, |
|||
{ |
|||
typeof(JsonField), |
|||
field => ResolveDefault("Json") |
|||
}, |
|||
{ |
|||
typeof(TagsField), |
|||
field => ResolveDefault("String") |
|||
}, |
|||
{ |
|||
typeof(GeolocationField), |
|||
field => ResolveDefault("Geolocation") |
|||
}, |
|||
{ |
|||
typeof(AssetsField), |
|||
field => ResolveAssets(assetListType) |
|||
}, |
|||
{ |
|||
typeof(ReferencesField), |
|||
field => ResolveReferences(field) |
|||
} |
|||
}; |
|||
|
|||
this.schemas = schemas.ToDictionary(x => x.Id); |
|||
|
|||
graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) }; |
|||
|
|||
foreach (var schemaType in schemaTypes.Values) |
|||
{ |
|||
schemaType.Initialize(); |
|||
} |
|||
} |
|||
|
|||
private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name) |
|||
{ |
|||
return (new NoopGraphType(name), new FuncFieldResolver<ContentFieldData, object>(c => c.Source.GetOrDefault(c.FieldName))); |
|||
} |
|||
|
|||
public IFieldResolver ResolveAssetUrl() |
|||
{ |
|||
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
|
|||
return context.UrlGenerator.GenerateAssetUrl(app, c.Source); |
|||
}); |
|||
|
|||
return resolver; |
|||
} |
|||
|
|||
public IFieldResolver ResolveAssetSourceUrl() |
|||
{ |
|||
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
|
|||
return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); |
|||
}); |
|||
|
|||
return resolver; |
|||
} |
|||
|
|||
public IFieldResolver ResolveAssetThumbnailUrl() |
|||
{ |
|||
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
|
|||
return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); |
|||
}); |
|||
|
|||
return resolver; |
|||
} |
|||
|
|||
public IFieldResolver ResolveContentUrl(ISchemaEntity schema) |
|||
{ |
|||
var resolver = new FuncFieldResolver<IContentEntity, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
|
|||
return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); |
|||
}); |
|||
|
|||
return resolver; |
|||
} |
|||
|
|||
private static ValueTuple<IGraphType, IFieldResolver> ResolveAssets(IGraphType assetListType) |
|||
{ |
|||
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
var contentIds = c.Source.GetOrDefault(c.FieldName); |
|||
|
|||
return context.GetReferencedAssetsAsync(contentIds); |
|||
}); |
|||
|
|||
return (assetListType, resolver); |
|||
} |
|||
|
|||
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(Field field) |
|||
{ |
|||
var schemaId = ((ReferencesField)field).Properties.SchemaId; |
|||
var schemaType = GetSchemaType(schemaId); |
|||
|
|||
if (schemaType == null) |
|||
{ |
|||
return (null, null); |
|||
} |
|||
|
|||
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
var contentIds = c.Source.GetOrDefault(c.FieldName); |
|||
|
|||
return context.GetReferencedContentsAsync(schemaId, contentIds); |
|||
}); |
|||
|
|||
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); |
|||
|
|||
return (schemaFieldType, resolver); |
|||
} |
|||
|
|||
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query) |
|||
{ |
|||
Guard.NotNull(context, nameof(context)); |
|||
|
|||
var result = await new DocumentExecuter().ExecuteAsync(options => |
|||
{ |
|||
options.Query = query.Query; |
|||
options.Schema = graphQLSchema; |
|||
options.Inputs = query.Variables?.ToInputs() ?? new Inputs(); |
|||
options.UserContext = context; |
|||
options.OperationName = query.OperationName; |
|||
}).ConfigureAwait(false); |
|||
|
|||
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray()); |
|||
} |
|||
|
|||
public IFieldPartitioning ResolvePartition(Partitioning key) |
|||
{ |
|||
return partitionResolver(key); |
|||
} |
|||
|
|||
public IGraphType GetAssetType() |
|||
{ |
|||
return assetType; |
|||
} |
|||
|
|||
public (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field) |
|||
{ |
|||
return fieldInfos[field.GetType()](field); |
|||
} |
|||
|
|||
public IGraphType GetSchemaType(Guid schemaId) |
|||
{ |
|||
var schema = schemas.GetOrDefault(schemaId); |
|||
|
|||
return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
// ==========================================================================
|
|||
// GraphQLQuery.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Newtonsoft.Json.Linq; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public class GraphQLQuery |
|||
{ |
|||
public string OperationName { get; set; } |
|||
|
|||
public string NamedQuery { get; set; } |
|||
|
|||
public string Query { get; set; } |
|||
|
|||
public JObject Variables { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
// ==========================================================================
|
|||
// GraphQLQueryContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Newtonsoft.Json.Linq; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public sealed class GraphQLQueryContext : QueryContext |
|||
{ |
|||
public IGraphQLUrlGenerator UrlGenerator { get; } |
|||
|
|||
public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user, |
|||
IGraphQLUrlGenerator urlGenerator) |
|||
: base(app, assetRepository, contentQuery, user) |
|||
{ |
|||
UrlGenerator = urlGenerator; |
|||
} |
|||
|
|||
public Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(JToken value) |
|||
{ |
|||
var ids = ParseIds(value); |
|||
|
|||
return GetReferencedAssetsAsync(ids); |
|||
} |
|||
|
|||
public Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, JToken value) |
|||
{ |
|||
var ids = ParseIds(value); |
|||
|
|||
return GetReferencedContentsAsync(schemaId, ids); |
|||
} |
|||
|
|||
private static ICollection<Guid> ParseIds(JToken value) |
|||
{ |
|||
try |
|||
{ |
|||
var result = new List<Guid>(); |
|||
|
|||
if (value is JArray) |
|||
{ |
|||
foreach (var id in value) |
|||
{ |
|||
result.Add(Guid.Parse(id.ToString())); |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
catch |
|||
{ |
|||
return new List<Guid>(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// ==========================================================================
|
|||
// SchemaGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public interface IGraphQLContext |
|||
{ |
|||
bool CanGenerateAssetSourceUrl { get; } |
|||
|
|||
IFieldPartitioning ResolvePartition(Partitioning key); |
|||
|
|||
IGraphType GetAssetType(); |
|||
|
|||
IGraphType GetSchemaType(Guid schemaId); |
|||
|
|||
IFieldResolver ResolveAssetUrl(); |
|||
|
|||
IFieldResolver ResolveAssetSourceUrl(); |
|||
|
|||
IFieldResolver ResolveAssetThumbnailUrl(); |
|||
|
|||
IFieldResolver ResolveContentUrl(ISchemaEntity schema); |
|||
|
|||
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// IGraphQLService.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public interface IGraphQLService |
|||
{ |
|||
Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, ClaimsPrincipal user, GraphQLQuery query); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
// ==========================================================================
|
|||
// IGraphQLUrlGenerator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL |
|||
{ |
|||
public interface IGraphQLUrlGenerator |
|||
{ |
|||
bool CanGenerateAssetSourceUrl { get; } |
|||
|
|||
string GenerateAssetUrl(IAppEntity app, IAssetEntity asset); |
|||
|
|||
string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset); |
|||
|
|||
string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset); |
|||
|
|||
string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content); |
|||
} |
|||
} |
|||
@ -0,0 +1,170 @@ |
|||
// ==========================================================================
|
|||
// AssetGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> |
|||
{ |
|||
public AssetGraphType(IGraphQLContext context) |
|||
{ |
|||
Name = "AssetDto"; |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "id", |
|||
Resolver = Resolver(x => x.Id.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The id of the asset." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "version", |
|||
Resolver = Resolver(x => x.Version), |
|||
ResolvedType = new NonNullGraphType(new IntGraphType()), |
|||
Description = "The version of the asset." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "created", |
|||
Resolver = Resolver(x => x.Created.ToDateTimeUtc()), |
|||
ResolvedType = new NonNullGraphType(new DateGraphType()), |
|||
Description = "The date and time when the asset has been created." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "createdBy", |
|||
Resolver = Resolver(x => x.CreatedBy.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The user that has created the asset." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "lastModified", |
|||
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), |
|||
ResolvedType = new NonNullGraphType(new DateGraphType()), |
|||
Description = "The date and time when the asset has been modified last." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "lastModifiedBy", |
|||
Resolver = Resolver(x => x.LastModifiedBy.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The user that has updated the asset last." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "mimeType", |
|||
Resolver = Resolver(x => x.MimeType), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The mime type." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "url", |
|||
Resolver = context.ResolveAssetUrl(), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The url to the asset." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "thumbnailUrl", |
|||
Resolver = context.ResolveAssetThumbnailUrl(), |
|||
ResolvedType = new StringGraphType(), |
|||
Description = "The thumbnail url to the asset." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "fileName", |
|||
Resolver = Resolver(x => x.FileName), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The file name." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "fileType", |
|||
Resolver = Resolver(x => x.FileName.FileType()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = "The file type." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "fileSize", |
|||
Resolver = Resolver(x => x.FileSize), |
|||
ResolvedType = new NonNullGraphType(new IntGraphType()), |
|||
Description = "The size of the file in bytes." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "fileVersion", |
|||
Resolver = Resolver(x => x.FileVersion), |
|||
ResolvedType = new NonNullGraphType(new IntGraphType()), |
|||
Description = "The version of the file." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "isImage", |
|||
Resolver = Resolver(x => x.IsImage), |
|||
ResolvedType = new NonNullGraphType(new BooleanGraphType()), |
|||
Description = "Determines of the created file is an image." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "pixelWidth", |
|||
Resolver = Resolver(x => x.PixelWidth), |
|||
ResolvedType = new IntGraphType(), |
|||
Description = "The width of the image in pixels if the asset is an image." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "pixelHeight", |
|||
Resolver = Resolver(x => x.PixelHeight), |
|||
ResolvedType = new IntGraphType(), |
|||
Description = "The height of the image in pixels if the asset is an image." |
|||
}); |
|||
|
|||
if (context.CanGenerateAssetSourceUrl) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "sourceUrl", |
|||
Resolver = context.ResolveAssetSourceUrl(), |
|||
ResolvedType = new StringGraphType(), |
|||
Description = "The source url of the asset." |
|||
}); |
|||
} |
|||
|
|||
Description = "An asset"; |
|||
} |
|||
|
|||
private static IFieldResolver Resolver(Func<IAssetEntity, object> action) |
|||
{ |
|||
return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
// ==========================================================================
|
|||
// ContentDataGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> |
|||
{ |
|||
public ContentDataGraphType(Schema schema, IGraphQLContext context) |
|||
{ |
|||
var schemaName = schema.Properties.Label.WithFallback(schema.Name); |
|||
|
|||
Name = $"{schema.Name.ToPascalCase()}DataDto"; |
|||
|
|||
foreach (var field in schema.Fields.Where(x => !x.IsHidden)) |
|||
{ |
|||
var fieldInfo = context.GetGraphType(field); |
|||
|
|||
if (fieldInfo.ResolveType != null) |
|||
{ |
|||
var fieldName = field.RawProperties.Label.WithFallback(field.Name); |
|||
|
|||
var fieldGraphType = new ObjectGraphType |
|||
{ |
|||
Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" |
|||
}; |
|||
|
|||
var partition = context.ResolvePartition(field.Partitioning); |
|||
|
|||
foreach (var partitionItem in partition) |
|||
{ |
|||
fieldGraphType.AddField(new FieldType |
|||
{ |
|||
Name = partitionItem.Key, |
|||
Resolver = fieldInfo.Resolver, |
|||
ResolvedType = fieldInfo.ResolveType, |
|||
Description = field.RawProperties.Hints |
|||
}); |
|||
} |
|||
|
|||
fieldGraphType.Description = $"The structure of the {fieldName} of a {schemaName} content type."; |
|||
|
|||
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name)); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = field.Name.ToCamelCase(), |
|||
Resolver = fieldResolver, |
|||
ResolvedType = fieldGraphType |
|||
}); |
|||
} |
|||
} |
|||
|
|||
Description = $"The structure of a {schemaName} content type."; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
// ==========================================================================
|
|||
// SchemaGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ContentGraphType : ObjectGraphType<IContentEntity> |
|||
{ |
|||
private readonly ISchemaEntity schema; |
|||
private readonly IGraphQLContext context; |
|||
|
|||
public ContentGraphType(ISchemaEntity schema, IGraphQLContext context) |
|||
{ |
|||
this.context = context; |
|||
this.schema = schema; |
|||
|
|||
Name = $"{schema.Name.ToPascalCase()}Dto"; |
|||
} |
|||
|
|||
public void Initialize() |
|||
{ |
|||
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.Name); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "id", |
|||
Resolver = Resolver(x => x.Id.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = $"The id of the {schemaName} content." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "version", |
|||
Resolver = Resolver(x => x.Version), |
|||
ResolvedType = new NonNullGraphType(new IntGraphType()), |
|||
Description = $"The version of the {schemaName} content." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "created", |
|||
Resolver = Resolver(x => x.Created.ToDateTimeUtc()), |
|||
ResolvedType = new NonNullGraphType(new DateGraphType()), |
|||
Description = $"The date and time when the {schemaName} content has been created." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "createdBy", |
|||
Resolver = Resolver(x => x.CreatedBy.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = $"The user that has created the {schemaName} content." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "lastModified", |
|||
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()), |
|||
ResolvedType = new NonNullGraphType(new DateGraphType()), |
|||
Description = $"The date and time when the {schemaName} content has been modified last." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "lastModifiedBy", |
|||
Resolver = Resolver(x => x.LastModifiedBy.ToString()), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = $"The user that has updated the {schemaName} content last." |
|||
}); |
|||
|
|||
AddField(new FieldType |
|||
{ |
|||
Name = "url", |
|||
Resolver = context.ResolveContentUrl(schema), |
|||
ResolvedType = new NonNullGraphType(new StringGraphType()), |
|||
Description = $"The url to the the {schemaName} content." |
|||
}); |
|||
|
|||
var dataType = new ContentDataGraphType(schema.SchemaDef, context); |
|||
|
|||
if (dataType.Fields.Any()) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "data", |
|||
Resolver = Resolver(x => x.Data), |
|||
ResolvedType = new NonNullGraphType(dataType), |
|||
Description = $"The data of the {schemaName} content." |
|||
}); |
|||
} |
|||
|
|||
Description = $"The structure of a {schemaName} content type."; |
|||
} |
|||
|
|||
private static IFieldResolver Resolver(Func<IContentEntity, object> action) |
|||
{ |
|||
return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
// ==========================================================================
|
|||
// GraphModelType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using GraphQL.Resolvers; |
|||
using GraphQL.Types; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class ContentQueryGraphType : ObjectGraphType |
|||
{ |
|||
public ContentQueryGraphType(IGraphQLContext graphQLContext, IEnumerable<ISchemaEntity> schemas) |
|||
{ |
|||
AddAssetFind(graphQLContext); |
|||
AddAssetsQuery(graphQLContext); |
|||
|
|||
foreach (var schema in schemas) |
|||
{ |
|||
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); |
|||
var schemaType = graphQLContext.GetSchemaType(schema.Id); |
|||
|
|||
AddContentFind(schema, schemaType, schemaName); |
|||
AddContentQuery(schema, schemaType, schemaName); |
|||
} |
|||
|
|||
Description = "The app queries."; |
|||
} |
|||
|
|||
private void AddAssetFind(IGraphQLContext graphQLContext) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "findAsset", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "id", |
|||
Description = "The id of the asset.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = graphQLContext.GetAssetType(), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
|||
|
|||
return context.FindAssetAsync(contentId); |
|||
}), |
|||
Description = "Find an asset by id." |
|||
}); |
|||
} |
|||
|
|||
private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = $"find{schema.Name.ToPascalCase()}Content", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "id", |
|||
Description = $"The id of the {schemaName} content.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = schemaType, |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString())); |
|||
|
|||
return context.FindContentAsync(schema.Id, contentId); |
|||
}), |
|||
Description = $"Find an {schemaName} content by id." |
|||
}); |
|||
} |
|||
|
|||
private void AddAssetsQuery(IGraphQLContext graphQLContext) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = "queryAssets", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "top", |
|||
Description = "Optional number of assets to take.", |
|||
DefaultValue = 20 |
|||
}, |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "skip", |
|||
Description = "Optional number of assets to skip.", |
|||
DefaultValue = 0 |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "search", |
|||
Description = "Optional query.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = new ListGraphType(new NonNullGraphType(graphQLContext.GetAssetType())), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
|
|||
var argTop = c.GetArgument("top", 20); |
|||
var argSkip = c.GetArgument("skip", 0); |
|||
var argQuery = c.GetArgument("search", string.Empty); |
|||
|
|||
return context.QueryAssetsAsync(argQuery, argSkip, argTop); |
|||
}), |
|||
Description = "Query assets items." |
|||
}); |
|||
} |
|||
|
|||
private void AddContentQuery(ISchemaEntity schema, IGraphType schemaType, string schemaName) |
|||
{ |
|||
AddField(new FieldType |
|||
{ |
|||
Name = $"query{schema.Name.ToPascalCase()}Contents", |
|||
Arguments = new QueryArguments |
|||
{ |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "top", |
|||
Description = "Optional number of contents to take.", |
|||
DefaultValue = 20 |
|||
}, |
|||
new QueryArgument(typeof(IntGraphType)) |
|||
{ |
|||
Name = "skip", |
|||
Description = "Optional number of contents to skip.", |
|||
DefaultValue = 0 |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "filter", |
|||
Description = "Optional OData filter.", |
|||
DefaultValue = string.Empty |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "search", |
|||
Description = "Optional OData full text search.", |
|||
DefaultValue = string.Empty |
|||
}, |
|||
new QueryArgument(typeof(StringGraphType)) |
|||
{ |
|||
Name = "orderby", |
|||
Description = "Optional OData order definition.", |
|||
DefaultValue = string.Empty |
|||
} |
|||
}, |
|||
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), |
|||
Resolver = new FuncFieldResolver<object>(c => |
|||
{ |
|||
var context = (GraphQLQueryContext)c.UserContext; |
|||
var contentQuery = BuildODataQuery(c); |
|||
|
|||
return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); |
|||
}), |
|||
Description = $"Query {schemaName} content items." |
|||
}); |
|||
} |
|||
|
|||
private static string BuildODataQuery(ResolveFieldContext c) |
|||
{ |
|||
var odataQuery = "?" + |
|||
string.Join("&", |
|||
c.Arguments |
|||
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) |
|||
.Select(x => $"${x.Key}={x.Value}")); |
|||
|
|||
return odataQuery; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// ==========================================================================
|
|||
// NoopGraphType.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using GraphQL.Language.AST; |
|||
using GraphQL.Types; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types |
|||
{ |
|||
public sealed class NoopGraphType : ScalarGraphType |
|||
{ |
|||
public NoopGraphType(string name) |
|||
{ |
|||
Name = name; |
|||
} |
|||
|
|||
public override object Serialize(object value) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
public override object ParseValue(object value) |
|||
{ |
|||
return value; |
|||
} |
|||
|
|||
public override object ParseLiteral(IValue value) |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// ==========================================================================
|
|||
// IContentEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public interface IContentEntity : |
|||
IEntity, |
|||
IEntityWithAppRef, |
|||
IEntityWithCreatedBy, |
|||
IEntityWithLastModifiedBy, |
|||
IEntityWithVersion |
|||
{ |
|||
Status Status { get; } |
|||
|
|||
NamedContentData Data { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// ==========================================================================
|
|||
// IContentQueryService.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public interface IContentQueryService |
|||
{ |
|||
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, HashSet<Guid> ids); |
|||
|
|||
Task<(ISchemaEntity Schema, long Total, IReadOnlyList<IContentEntity> Items)> QueryWithCountAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, bool archived, string query); |
|||
|
|||
Task<(ISchemaEntity Schema, IContentEntity Content)> FindContentAsync(IAppEntity app, string schemaIdOrName, ClaimsPrincipal user, Guid id); |
|||
|
|||
Task<ISchemaEntity> FindSchemaAsync(IAppEntity app, string schemaIdOrName); |
|||
} |
|||
} |
|||
@ -0,0 +1,146 @@ |
|||
// ==========================================================================
|
|||
// QueryContext.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class QueryContext |
|||
{ |
|||
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>(); |
|||
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>(); |
|||
private readonly IContentQueryService contentQuery; |
|||
private readonly IAssetRepository assetRepository; |
|||
private readonly IAppEntity app; |
|||
private readonly ClaimsPrincipal user; |
|||
|
|||
public QueryContext( |
|||
IAppEntity app, |
|||
IAssetRepository assetRepository, |
|||
IContentQueryService contentQuery, |
|||
ClaimsPrincipal user) |
|||
{ |
|||
Guard.NotNull(assetRepository, nameof(assetRepository)); |
|||
Guard.NotNull(contentQuery, nameof(contentQuery)); |
|||
Guard.NotNull(app, nameof(app)); |
|||
Guard.NotNull(user, nameof(user)); |
|||
|
|||
this.assetRepository = assetRepository; |
|||
this.contentQuery = contentQuery; |
|||
|
|||
this.user = user; |
|||
|
|||
this.app = app; |
|||
} |
|||
|
|||
public async Task<IAssetEntity> FindAssetAsync(Guid id) |
|||
{ |
|||
var asset = cachedAssets.GetOrDefault(id); |
|||
|
|||
if (asset == null) |
|||
{ |
|||
asset = await assetRepository.FindAssetAsync(id); |
|||
|
|||
if (asset != null) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
} |
|||
|
|||
return asset; |
|||
} |
|||
|
|||
public async Task<IContentEntity> FindContentAsync(Guid schemaId, Guid id) |
|||
{ |
|||
var content = cachedContents.GetOrDefault(id); |
|||
|
|||
if (content == null) |
|||
{ |
|||
content = (await contentQuery.FindContentAsync(app, schemaId.ToString(), user, id)).Content; |
|||
|
|||
if (content != null) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
} |
|||
|
|||
return content; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IAssetEntity>> QueryAssetsAsync(string query, int skip = 0, int take = 10) |
|||
{ |
|||
var assets = await assetRepository.QueryAsync(app.Id, null, null, query, take, skip); |
|||
|
|||
foreach (var asset in assets) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
|
|||
return assets; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query) |
|||
{ |
|||
var contents = await contentQuery.QueryWithCountAsync(app, schemaIdOrName, user, false, query); |
|||
|
|||
foreach (var content in contents.Items) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
|
|||
return contents.Items; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
var notLoadedAssets = new HashSet<Guid>(ids.Where(id => !cachedAssets.ContainsKey(id))); |
|||
|
|||
if (notLoadedAssets.Count > 0) |
|||
{ |
|||
var assets = await assetRepository.QueryAsync(app.Id, null, notLoadedAssets, null, int.MaxValue); |
|||
|
|||
foreach (var asset in assets) |
|||
{ |
|||
cachedAssets[asset.Id] = asset; |
|||
} |
|||
} |
|||
|
|||
return ids.Select(id => cachedAssets.GetOrDefault(id)).Where(x => x != null).ToList(); |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids) |
|||
{ |
|||
Guard.NotNull(ids, nameof(ids)); |
|||
|
|||
var notLoadedContents = new HashSet<Guid>(ids.Where(id => !cachedContents.ContainsKey(id))); |
|||
|
|||
if (notLoadedContents.Count > 0) |
|||
{ |
|||
var contents = await contentQuery.QueryWithCountAsync(app, schemaId.ToString(), user, false, notLoadedContents); |
|||
|
|||
foreach (var content in contents.Items) |
|||
{ |
|||
cachedContents[content.Id] = content; |
|||
} |
|||
} |
|||
|
|||
return ids.Select(id => cachedContents.GetOrDefault(id)).Where(x => x != null).ToList(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
// ==========================================================================
|
|||
// IContentRepository.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.OData.UriParser; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.Repositories |
|||
{ |
|||
public interface IContentRepository |
|||
{ |
|||
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); |
|||
|
|||
Task<IReadOnlyList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); |
|||
|
|||
Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds); |
|||
|
|||
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids); |
|||
|
|||
Task<long> CountAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery); |
|||
|
|||
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id); |
|||
} |
|||
} |
|||
@ -0,0 +1,244 @@ |
|||
// ==========================================================================
|
|||
// AppCommandMiddlewareTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Shared.Users; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public class AppCommandMiddlewareTests : HandlerTestBase<AppDomainObject> |
|||
{ |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>(); |
|||
private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake<IAppPlanBillingManager>(); |
|||
private readonly IUserResolver userResolver = A.Fake<IUserResolver>(); |
|||
private readonly AppCommandMiddleware sut; |
|||
private readonly AppDomainObject app; |
|||
private readonly Language language = Language.DE; |
|||
private readonly string contributorId = Guid.NewGuid().ToString(); |
|||
private readonly string clientName = "client"; |
|||
|
|||
public AppCommandMiddlewareTests() |
|||
{ |
|||
app = new AppDomainObject(); |
|||
|
|||
A.CallTo(() => appProvider.GetAppAsync(AppName)) |
|||
.Returns((IAppEntity)null); |
|||
|
|||
A.CallTo(() => userResolver.FindByIdAsync(contributorId)) |
|||
.Returns(A.Fake<IUser>()); |
|||
|
|||
sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Create_should_create_domain_object() |
|||
{ |
|||
var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); |
|||
|
|||
await TestCreate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.Equal(AppId, context.Result<EntityCreatedResult<Guid>>().IdOrValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AssignContributor_should_update_domain_object_if_user_found() |
|||
{ |
|||
A.CallTo(() => appPlansProvider.GetPlan(null)) |
|||
.Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); |
|||
|
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RemoveContributor_should_update_domain_object() |
|||
{ |
|||
CreateApp() |
|||
.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); |
|||
|
|||
var context = CreateContextForCommand(new RemoveContributor { ContributorId = contributorId }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AttachClient_should_update_domain_object() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new AttachClient { Id = clientName }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RenameClient_should_update_domain_object() |
|||
{ |
|||
CreateApp() |
|||
.AttachClient(CreateCommand(new AttachClient { Id = clientName })); |
|||
|
|||
var context = CreateContextForCommand(new UpdateClient { Id = clientName, Name = "New Name" }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RevokeClient_should_update_domain_object() |
|||
{ |
|||
CreateApp() |
|||
.AttachClient(CreateCommand(new AttachClient { Id = clientName })); |
|||
|
|||
var context = CreateContextForCommand(new RevokeClient { Id = clientName }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ChangePlan_should_update_domain_object() |
|||
{ |
|||
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
|||
.Returns(true); |
|||
|
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ChangePlan_should_not_make_update_for_redirect_result() |
|||
{ |
|||
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
|||
.Returns(true); |
|||
|
|||
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
|||
.Returns(CreateRedirectResult()); |
|||
|
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
Assert.Null(app.State.Plan); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task ChangePlan_should_not_call_billing_manager_for_callback() |
|||
{ |
|||
A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) |
|||
.Returns(true); |
|||
|
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
|
|||
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, "my-plan")) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task AddLanguage_should_update_domain_object() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
var context = CreateContextForCommand(new AddLanguage { Language = language }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task RemoveLanguage_should_update_domain_object() |
|||
{ |
|||
CreateApp() |
|||
.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
|||
|
|||
var context = CreateContextForCommand(new RemoveLanguage { Language = language }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task UpdateLanguage_should_update_domain_object() |
|||
{ |
|||
CreateApp() |
|||
.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
|||
|
|||
var context = CreateContextForCommand(new UpdateLanguage { Language = language }); |
|||
|
|||
await TestUpdate(app, async _ => |
|||
{ |
|||
await sut.HandleAsync(context); |
|||
}); |
|||
} |
|||
|
|||
private AppDomainObject CreateApp() |
|||
{ |
|||
app.Create(CreateCommand(new CreateApp { AppId = AppId, Name = AppName })); |
|||
|
|||
return app; |
|||
} |
|||
|
|||
private static Task<IChangePlanResult> CreateRedirectResult() |
|||
{ |
|||
return Task.FromResult<IChangePlanResult>(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,284 @@ |
|||
// ==========================================================================
|
|||
// AppDomainObjectTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Domain.Apps.Events.Apps; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public class AppDomainObjectTests : HandlerTestBase<AppDomainObject> |
|||
{ |
|||
private readonly AppDomainObject sut; |
|||
private readonly string contributorId = Guid.NewGuid().ToString(); |
|||
private readonly string clientId = "client"; |
|||
private readonly string clientNewName = "My Client"; |
|||
private readonly string planId = "premium"; |
|||
|
|||
public AppDomainObjectTests() |
|||
{ |
|||
sut = new AppDomainObject(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_throw_exception_if_created() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.Create(CreateCommand(new CreateApp { Name = AppName })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Create_should_specify_name_and_owner() |
|||
{ |
|||
sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); |
|||
|
|||
Assert.Equal(AppName, sut.State.Name); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppCreated { Name = AppName }), |
|||
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), |
|||
CreateEvent(new AppLanguageAdded { Language = Language.EN }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangePlan_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ChangePlan_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppPlanChanged { PlanId = planId }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AssignContributor_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AssignContributor_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RemoveContributor_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RemoveContributor_should_create_events_and_remove_contributor() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); |
|||
sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); |
|||
|
|||
sut.GetUncomittedEvents().Skip(1) |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AttachClient_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AttachClient_should_create_events() |
|||
{ |
|||
var command = new AttachClient { Id = clientId }; |
|||
|
|||
CreateApp(); |
|||
|
|||
sut.AttachClient(CreateCommand(command)); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RevokeClient_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RevokeClient_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
CreateClient(); |
|||
|
|||
sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppClientRevoked { Id = clientId }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
CreateClient(); |
|||
|
|||
sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), |
|||
CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AddLanguage_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void AddLanguage_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
|
|||
sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppLanguageAdded { Language = Language.DE }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RemoveLanguage_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RemoveLanguage_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
CreateLanguage(Language.DE); |
|||
|
|||
sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppLanguageRemoved { Language = Language.DE }) |
|||
); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateLanguage_should_throw_exception_if_not_created() |
|||
{ |
|||
Assert.Throws<DomainException>(() => |
|||
{ |
|||
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); |
|||
}); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateLanguage_should_create_events() |
|||
{ |
|||
CreateApp(); |
|||
CreateLanguage(Language.DE); |
|||
|
|||
sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } })); |
|||
|
|||
sut.GetUncomittedEvents() |
|||
.ShouldHaveSameEvents( |
|||
CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } }) |
|||
); |
|||
} |
|||
|
|||
private void CreateApp() |
|||
{ |
|||
sut.Create(CreateCommand(new CreateApp { Name = AppName })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void CreateClient() |
|||
{ |
|||
sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
|
|||
private void CreateLanguage(Language language) |
|||
{ |
|||
sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); |
|||
sut.ClearUncommittedEvents(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// ==========================================================================
|
|||
// AppEventTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Domain.Apps.Events.Apps; |
|||
using Squidex.Domain.Apps.Events.Apps.Old; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable CS0612 // Type or member is obsolete
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public class AppEventTests |
|||
{ |
|||
private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); |
|||
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app"); |
|||
|
|||
[Fact] |
|||
public void Should_migrate_client_changed_as_reader_to_client_updated() |
|||
{ |
|||
var source = CreateEvent(new AppClientChanged { IsReader = true }); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_migrate_client_changed_as_writer_to_client_updated() |
|||
{ |
|||
var source = CreateEvent(new AppClientChanged { IsReader = false }); |
|||
|
|||
source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); |
|||
} |
|||
|
|||
private T CreateEvent<T>(T contentEvent) where T : AppEvent |
|||
{ |
|||
contentEvent.Actor = actor; |
|||
contentEvent.AppId = appId; |
|||
|
|||
return contentEvent; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,159 @@ |
|||
// ==========================================================================
|
|||
// ConfigAppLimitsProviderTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Linq; |
|||
using FakeItEasy; |
|||
using FluentAssertions; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public class ConfigAppLimitsProviderTests |
|||
{ |
|||
private static readonly ConfigAppLimitsPlan InfinitePlan = new ConfigAppLimitsPlan |
|||
{ |
|||
Id = "infinite", |
|||
Name = "Infinite", |
|||
MaxApiCalls = -1, |
|||
MaxAssetSize = -1, |
|||
MaxContributors = -1 |
|||
}; |
|||
|
|||
private static readonly ConfigAppLimitsPlan FreePlan = new ConfigAppLimitsPlan |
|||
{ |
|||
Id = "free", |
|||
Name = "Free", |
|||
MaxApiCalls = 50000, |
|||
MaxAssetSize = 1024 * 1024 * 10, |
|||
MaxContributors = 2 |
|||
}; |
|||
|
|||
private static readonly ConfigAppLimitsPlan BasicPlan = new ConfigAppLimitsPlan |
|||
{ |
|||
Id = "basic", |
|||
Name = "Basic", |
|||
MaxApiCalls = 150000, |
|||
MaxAssetSize = 1024 * 1024 * 2, |
|||
MaxContributors = 5 |
|||
}; |
|||
|
|||
private static readonly ConfigAppLimitsPlan[] Plans = { BasicPlan, FreePlan }; |
|||
|
|||
[Fact] |
|||
public void Should_return_plans() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
Plans.OrderBy(x => x.MaxApiCalls).ShouldBeEquivalentTo(sut.GetAvailablePlans()); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(null)] |
|||
[InlineData("my-plan")] |
|||
public void Should_return_infinite_if_nothing_configured(string planId) |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Enumerable.Empty<ConfigAppLimitsPlan>()); |
|||
|
|||
var plan = sut.GetPlanForApp(CreateApp(planId)); |
|||
|
|||
plan.ShouldBeEquivalentTo(InfinitePlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_fitting_app_plan() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var plan = sut.GetPlanForApp(CreateApp("basic")); |
|||
|
|||
plan.ShouldBeEquivalentTo(BasicPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_smallest_plan_if_none_fits() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var plan = sut.GetPlanForApp(CreateApp("enterprise")); |
|||
|
|||
plan.ShouldBeEquivalentTo(FreePlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_second_plan_for_upgrade_if_plan_is_null() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var upgradePlan = sut.GetPlanUpgrade(null); |
|||
|
|||
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_second_plan_for_upgrade_if_plan_not_found() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("enterprise")); |
|||
|
|||
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_return_plan_for_upgrade_if_plan_is_highest_plan() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("basic")); |
|||
|
|||
Assert.Null(upgradePlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_next_plan_if_plan_is_upgradeable() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
var upgradePlan = sut.GetPlanUpgradeForApp(CreateApp("free")); |
|||
|
|||
upgradePlan.ShouldBeEquivalentTo(BasicPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_check_plan_exists() |
|||
{ |
|||
var sut = new ConfigAppPlansProvider(Plans); |
|||
|
|||
Assert.True(sut.IsConfiguredPlan("basic")); |
|||
Assert.True(sut.IsConfiguredPlan("free")); |
|||
|
|||
Assert.False(sut.IsConfiguredPlan("infinite")); |
|||
Assert.False(sut.IsConfiguredPlan("invalid")); |
|||
Assert.False(sut.IsConfiguredPlan(null)); |
|||
} |
|||
|
|||
private static IAppEntity CreateApp(string plan) |
|||
{ |
|||
var app = A.Dummy<IAppEntity>(); |
|||
|
|||
if (plan != null) |
|||
{ |
|||
A.CallTo(() => app.Plan).Returns(new AppPlan(new RefToken("user", "me"), plan)); |
|||
} |
|||
else |
|||
{ |
|||
A.CallTo(() => app.Plan).Returns(null); |
|||
} |
|||
|
|||
return app; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,142 @@ |
|||
// ==========================================================================
|
|||
// GuardAppClientsTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1310 // Field names must not contain underscore
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
|||
{ |
|||
public class GuardAppClientsTests |
|||
{ |
|||
private readonly AppClients clients_0 = AppClients.Empty; |
|||
|
|||
[Fact] |
|||
public void CanAttach_should_throw_execption_if_client_id_is_null() |
|||
{ |
|||
var command = new AttachClient(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanAttach(clients_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanAttach_should_throw_exception_if_client_already_exists() |
|||
{ |
|||
var command = new AttachClient { Id = "android" }; |
|||
|
|||
var clients_1 = clients_0.Add("android", "secret"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanAttach(clients_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanAttach_should_not_throw_exception_if_client_is_free() |
|||
{ |
|||
var command = new AttachClient { Id = "ios" }; |
|||
|
|||
var clients_1 = clients_0.Add("android", "secret"); |
|||
|
|||
GuardAppClients.CanAttach(clients_1, command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRevoke_should_throw_execption_if_client_id_is_null() |
|||
{ |
|||
var command = new RevokeClient(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanRevoke(clients_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRevoke_should_throw_exception_if_client_is_not_found() |
|||
{ |
|||
var command = new RevokeClient { Id = "ios" }; |
|||
|
|||
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanRevoke(clients_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRevoke_should_not_throw_exception_if_client_is_found() |
|||
{ |
|||
var command = new RevokeClient { Id = "ios" }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
GuardAppClients.CanRevoke(clients_1, command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdate_should_throw_execption_if_client_id_is_null() |
|||
{ |
|||
var command = new UpdateClient(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_client_is_not_found() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios", Name = "iOS" }; |
|||
|
|||
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(clients_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios" }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_client_has_same_name() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios", Name = "ios" }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_throw_exception_if_client_has_same_permission() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppClients.CanUpdate(clients_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void UpdateClient_should_not_throw_exception_if_command_is_valid() |
|||
{ |
|||
var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; |
|||
|
|||
var clients_1 = clients_0.Add("ios", "secret"); |
|||
|
|||
GuardAppClients.CanUpdate(clients_1, command); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,158 @@ |
|||
// ==========================================================================
|
|||
// GuardAppContributorsTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1310 // Field names must not contain underscore
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
|||
{ |
|||
public class GuardAppContributorsTests |
|||
{ |
|||
private readonly IUserResolver users = A.Fake<IUserResolver>(); |
|||
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>(); |
|||
private readonly AppContributors contributors_0 = AppContributors.Empty; |
|||
|
|||
public GuardAppContributorsTests() |
|||
{ |
|||
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
|||
.Returns(A.Fake<IUser>()); |
|||
|
|||
A.CallTo(() => appPlan.MaxContributors) |
|||
.Returns(10); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_throw_exception_if_contributor_id_is_null() |
|||
{ |
|||
var command = new AssignContributor(); |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_throw_exception_if_permission_not_valid() |
|||
{ |
|||
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() |
|||
{ |
|||
var command = new AssignContributor { ContributorId = "1" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_throw_exception_if_user_not_found() |
|||
{ |
|||
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
|||
.Returns(Task.FromResult<IUser>(null)); |
|||
|
|||
var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_throw_exception_if_contributor_max_reached() |
|||
{ |
|||
A.CallTo(() => appPlan.MaxContributors) |
|||
.Returns(2); |
|||
|
|||
var command = new AssignContributor { ContributorId = "3" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
|||
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_not_throw_exception_if_user_found() |
|||
{ |
|||
var command = new AssignContributor { ContributorId = "1" }; |
|||
|
|||
return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() |
|||
{ |
|||
var command = new AssignContributor { ContributorId = "1" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); |
|||
|
|||
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() |
|||
{ |
|||
A.CallTo(() => appPlan.MaxContributors) |
|||
.Returns(2); |
|||
|
|||
var command = new AssignContributor { ContributorId = "1" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Editor); |
|||
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
|||
|
|||
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemove_should_throw_exception_if_contributor_id_is_null() |
|||
{ |
|||
var command = new RemoveContributor(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppContributors.CanRemove(contributors_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemove_should_throw_exception_if_contributor_not_found() |
|||
{ |
|||
var command = new RemoveContributor { ContributorId = "1" }; |
|||
|
|||
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppContributors.CanRemove(contributors_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemove_should_throw_exception_if_contributor_is_only_owner() |
|||
{ |
|||
var command = new RemoveContributor { ContributorId = "1" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
|||
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Editor); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppContributors.CanRemove(contributors_2, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() |
|||
{ |
|||
var command = new RemoveContributor { ContributorId = "1" }; |
|||
|
|||
var contributors_1 = contributors_0.Assign("1", AppContributorPermission.Owner); |
|||
var contributors_2 = contributors_1.Assign("2", AppContributorPermission.Owner); |
|||
|
|||
GuardAppContributors.CanRemove(contributors_2, command); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
// ==========================================================================
|
|||
// GuardAppLanguagesTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1310 // Field names must not contain underscore
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
|||
{ |
|||
public class GuardAppLanguagesTests |
|||
{ |
|||
private readonly LanguagesConfig languages_0 = LanguagesConfig.Build(Language.DE); |
|||
|
|||
[Fact] |
|||
public void CanAddLanguage_should_throw_exception_if_language_is_null() |
|||
{ |
|||
var command = new AddLanguage(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanAdd(languages_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanAddLanguage_should_throw_exception_if_language_already_added() |
|||
{ |
|||
var command = new AddLanguage { Language = Language.DE }; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanAdd(languages_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanAddLanguage_should_not_throw_exception_if_language_valid() |
|||
{ |
|||
var command = new AddLanguage { Language = Language.EN }; |
|||
|
|||
GuardAppLanguages.CanAdd(languages_0, command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemoveLanguage_should_throw_exception_if_language_is_null() |
|||
{ |
|||
var command = new RemoveLanguage(); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemoveLanguage_should_throw_exception_if_language_not_found() |
|||
{ |
|||
var command = new RemoveLanguage { Language = Language.EN }; |
|||
|
|||
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemoveLanguage_should_throw_exception_if_language_is_master() |
|||
{ |
|||
var command = new RemoveLanguage { Language = Language.DE }; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanRemove(languages_0, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() |
|||
{ |
|||
var command = new RemoveLanguage { Language = Language.EN }; |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
GuardAppLanguages.CanRemove(languages_1, command); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdateLanguage_should_throw_exception_if_language_is_null() |
|||
{ |
|||
var command = new UpdateLanguage(); |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() |
|||
{ |
|||
var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() |
|||
{ |
|||
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.IT } }; |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdateLanguage_should_throw_exception_if_not_found() |
|||
{ |
|||
var command = new UpdateLanguage { Language = Language.IT }; |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppLanguages.CanUpdate(languages_1, command)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() |
|||
{ |
|||
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } }; |
|||
|
|||
var languages_1 = languages_0.Set(new LanguageConfig(Language.EN)); |
|||
|
|||
GuardAppLanguages.CanUpdate(languages_1, command); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// ==========================================================================
|
|||
// GuardAppTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Shared.Users; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
|||
{ |
|||
public class GuardAppTests |
|||
{ |
|||
private readonly IAppProvider apps = A.Fake<IAppProvider>(); |
|||
private readonly IUserResolver users = A.Fake<IUserResolver>(); |
|||
private readonly IAppPlansProvider appPlans = A.Fake<IAppPlansProvider>(); |
|||
|
|||
public GuardAppTests() |
|||
{ |
|||
A.CallTo(() => apps.GetAppAsync("new-app")) |
|||
.Returns(Task.FromResult<IAppEntity>(null)); |
|||
|
|||
A.CallTo(() => users.FindByIdAsync(A<string>.Ignored)) |
|||
.Returns(A.Fake<IUser>()); |
|||
|
|||
A.CallTo(() => appPlans.GetPlan("free")) |
|||
.Returns(A.Fake<IAppLimitsPlan>()); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanCreate_should_throw_exception_if_name_already_in_use() |
|||
{ |
|||
A.CallTo(() => apps.GetAppAsync("new-app")) |
|||
.Returns(A.Fake<IAppEntity>()); |
|||
|
|||
var command = new CreateApp { Name = "new-app" }; |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardApp.CanCreate(command, apps)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanCreate_should_throw_exception_if_name_not_valid() |
|||
{ |
|||
var command = new CreateApp { Name = "INVALID NAME" }; |
|||
|
|||
return Assert.ThrowsAsync<ValidationException>(() => GuardApp.CanCreate(command, apps)); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task CanCreate_should_not_throw_exception_if_app_name_is_free() |
|||
{ |
|||
var command = new CreateApp { Name = "new-app" }; |
|||
|
|||
return GuardApp.CanCreate(command, apps); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangePlan_should_throw_exception_if_plan_id_null() |
|||
{ |
|||
var command = new ChangePlan { Actor = new RefToken("user", "me") }; |
|||
|
|||
AppPlan plan = null; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangePlan_should_throw_exception_if_plan_not_found() |
|||
{ |
|||
A.CallTo(() => appPlans.GetPlan("free")) |
|||
.Returns(null); |
|||
|
|||
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
|||
|
|||
AppPlan plan = null; |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() |
|||
{ |
|||
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
|||
|
|||
var plan = new AppPlan(new RefToken("user", "other"), "premium"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangePlan_should_throw_exception_if_plan_is_the_same() |
|||
{ |
|||
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
|||
|
|||
var plan = new AppPlan(new RefToken("user", "me"), "free"); |
|||
|
|||
Assert.Throws<ValidationException>(() => GuardApp.CanChangePlan(command, plan, appPlans)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() |
|||
{ |
|||
var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; |
|||
|
|||
var plan = new AppPlan(new RefToken("user", "me"), "premium"); |
|||
|
|||
GuardApp.CanChangePlan(command, plan, appPlans); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// ==========================================================================
|
|||
// NoopAppPlanBillingManagerTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps |
|||
{ |
|||
public class NoopAppPlanBillingManagerTests |
|||
{ |
|||
private readonly NoopAppPlanBillingManager sut = new NoopAppPlanBillingManager(); |
|||
|
|||
[Fact] |
|||
public void Should_not_have_portal() |
|||
{ |
|||
Assert.False(sut.HasPortal); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_do_nothing_when_changing_plan() |
|||
{ |
|||
await sut.ChangePlanAsync(null, Guid.Empty, null, null); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_return_portal_link() |
|||
{ |
|||
Assert.Equal(string.Empty, await sut.GetPortalLinkAsync(null)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,220 @@ |
|||
// ==========================================================================
|
|||
// ContentQueryServiceTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Microsoft.OData.UriParser; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|||
using Squidex.Domain.Apps.Entities.Contents.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Security; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ContentQueryServiceTests |
|||
{ |
|||
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>(); |
|||
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); |
|||
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); |
|||
private readonly IContentEntity content = A.Fake<IContentEntity>(); |
|||
private readonly IAppEntity app = A.Fake<IAppEntity>(); |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly Guid appId = Guid.NewGuid(); |
|||
private readonly Guid schemaId = Guid.NewGuid(); |
|||
private readonly Guid contentId = Guid.NewGuid(); |
|||
private readonly string appName = "my-app"; |
|||
private readonly NamedContentData data = new NamedContentData(); |
|||
private readonly NamedContentData transformedData = new NamedContentData(); |
|||
private readonly ClaimsPrincipal user; |
|||
private readonly ClaimsIdentity identity = new ClaimsIdentity(); |
|||
private readonly EdmModelBuilder modelBuilder = A.Fake<EdmModelBuilder>(); |
|||
private readonly ContentQueryService sut; |
|||
|
|||
public ContentQueryServiceTests() |
|||
{ |
|||
user = new ClaimsPrincipal(identity); |
|||
|
|||
A.CallTo(() => app.Id).Returns(appId); |
|||
A.CallTo(() => app.Name).Returns(appName); |
|||
|
|||
A.CallTo(() => content.Id).Returns(contentId); |
|||
A.CallTo(() => content.Data).Returns(data); |
|||
A.CallTo(() => content.Status).Returns(Status.Published); |
|||
|
|||
sut = new ContentQueryService(contentRepository, appProvider, scriptEngine, modelBuilder); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_schema_from_id_if_string_is_guid() |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
|||
.Returns(schema); |
|||
|
|||
var result = await sut.FindSchemaAsync(app, schemaId.ToString()); |
|||
|
|||
Assert.Equal(schema, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_schema_from_name_if_string_not_guid() |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) |
|||
.Returns(schema); |
|||
|
|||
var result = await sut.FindSchemaAsync(app, "my-schema"); |
|||
|
|||
Assert.Equal(schema, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_if_schema_not_found() |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, "my-schema", false)) |
|||
.Returns((ISchemaEntity)null); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.FindSchemaAsync(app, "my-schema")); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_content_from_repository_and_transform() |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
|||
.Returns(schema); |
|||
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) |
|||
.Returns(content); |
|||
|
|||
A.CallTo(() => schema.ScriptQuery) |
|||
.Returns("<script-query>"); |
|||
|
|||
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>")) |
|||
.Returns(transformedData); |
|||
|
|||
var result = await sut.FindContentAsync(app, schemaId.ToString(), user, contentId); |
|||
|
|||
Assert.Equal(schema, result.Schema); |
|||
Assert.Equal(data, result.Content.Data); |
|||
Assert.Equal(content.Id, result.Content.Id); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_throw_if_content_to_find_does_not_exist() |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
|||
.Returns(schema); |
|||
A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) |
|||
.Returns((IContentEntity)null); |
|||
|
|||
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindContentAsync(app, schemaId.ToString(), user, contentId)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_contents_with_ids_from_repository_and_transform() |
|||
{ |
|||
await TestManyIdRequest(true, false, new HashSet<Guid> { Guid.NewGuid() }, Status.Draft, Status.Published); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_non_archived_contents_from_repository_and_transform() |
|||
{ |
|||
await TestManyRequest(true, false, Status.Draft, Status.Published); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_archived_contents_from_repository_and_transform() |
|||
{ |
|||
await TestManyRequest(true, true, Status.Archived); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_draft_contents_from_repository_and_transform() |
|||
{ |
|||
await TestManyRequest(false, false, Status.Published); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend() |
|||
{ |
|||
await TestManyRequest(false, true, Status.Published); |
|||
} |
|||
|
|||
private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status) |
|||
{ |
|||
SetupClaims(isFrontend); |
|||
|
|||
SetupFakeWithOdataQuery(status); |
|||
SetupFakeWithScripting(); |
|||
|
|||
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, string.Empty); |
|||
|
|||
Assert.Equal(123, result.Total); |
|||
Assert.Equal(schema, result.Schema); |
|||
Assert.Equal(data, result.Items[0].Data); |
|||
Assert.Equal(content.Id, result.Items[0].Id); |
|||
} |
|||
|
|||
private async Task TestManyIdRequest(bool isFrontend, bool archive, HashSet<Guid> ids, params Status[] status) |
|||
{ |
|||
SetupClaims(isFrontend); |
|||
|
|||
SetupFakeWithIdQuery(status, ids); |
|||
SetupFakeWithScripting(); |
|||
|
|||
var result = await sut.QueryWithCountAsync(app, schemaId.ToString(), user, archive, ids); |
|||
|
|||
Assert.Equal(123, result.Total); |
|||
Assert.Equal(schema, result.Schema); |
|||
Assert.Equal(data, result.Items[0].Data); |
|||
Assert.Equal(content.Id, result.Items[0].Id); |
|||
} |
|||
|
|||
private void SetupClaims(bool isFrontend) |
|||
{ |
|||
if (isFrontend) |
|||
{ |
|||
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend")); |
|||
} |
|||
} |
|||
|
|||
private void SetupFakeWithIdQuery(Status[] status, HashSet<Guid> ids) |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
|||
.Returns(schema); |
|||
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids)) |
|||
.Returns(new List<IContentEntity> { content }); |
|||
A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids)) |
|||
.Returns(123); |
|||
} |
|||
|
|||
private void SetupFakeWithOdataQuery(Status[] status) |
|||
{ |
|||
A.CallTo(() => appProvider.GetSchemaAsync(appName, schemaId, false)) |
|||
.Returns(schema); |
|||
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) |
|||
.Returns(new List<IContentEntity> { content }); |
|||
A.CallTo(() => contentRepository.CountAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<ODataUriParser>.Ignored)) |
|||
.Returns(123); |
|||
} |
|||
|
|||
private void SetupFakeWithScripting() |
|||
{ |
|||
A.CallTo(() => schema.ScriptQuery) |
|||
.Returns("<script-query>"); |
|||
|
|||
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, data)), "<query-script>")) |
|||
.Returns(transformedData); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,689 @@ |
|||
// ==========================================================================
|
|||
// GraphQLTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Newtonsoft.Json; |
|||
using Newtonsoft.Json.Linq; |
|||
using NodaTime.Extensions; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Repositories; |
|||
using Squidex.Domain.Apps.Entities.Contents.GraphQL; |
|||
using Squidex.Domain.Apps.Entities.Contents.TestData; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class GraphQLTests |
|||
{ |
|||
private static readonly Guid schemaId = Guid.NewGuid(); |
|||
private static readonly Guid appId = Guid.NewGuid(); |
|||
private static readonly string appName = "my-app"; |
|||
private readonly Schema schemaDef; |
|||
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>(); |
|||
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>(); |
|||
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); |
|||
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly IAppEntity app = A.Dummy<IAppEntity>(); |
|||
private readonly ClaimsPrincipal user = new ClaimsPrincipal(); |
|||
private readonly IGraphQLService sut; |
|||
|
|||
public GraphQLTests() |
|||
{ |
|||
schemaDef = |
|||
new Schema("my-schema") |
|||
.AddField(new JsonField(1, "my-json", Partitioning.Invariant, |
|||
new JsonFieldProperties())) |
|||
.AddField(new StringField(2, "my-string", Partitioning.Language, |
|||
new StringFieldProperties())) |
|||
.AddField(new NumberField(3, "my-number", Partitioning.Invariant, |
|||
new NumberFieldProperties())) |
|||
.AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, |
|||
new AssetsFieldProperties())) |
|||
.AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, |
|||
new BooleanFieldProperties())) |
|||
.AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, |
|||
new DateTimeFieldProperties())) |
|||
.AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, |
|||
new ReferencesFieldProperties { SchemaId = schemaId })) |
|||
.AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, |
|||
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) |
|||
.AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, |
|||
new GeolocationFieldProperties())) |
|||
.AddField(new TagsField(11, "my-tags", Partitioning.Invariant, |
|||
new TagsFieldProperties())); |
|||
|
|||
A.CallTo(() => app.Id).Returns(appId); |
|||
A.CallTo(() => app.Name).Returns(appName); |
|||
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE)); |
|||
|
|||
A.CallTo(() => schema.Id).Returns(schemaId); |
|||
A.CallTo(() => schema.Name).Returns(schemaDef.Name); |
|||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|||
A.CallTo(() => schema.IsPublished).Returns(true); |
|||
A.CallTo(() => schema.ScriptQuery).Returns("<script-query>"); |
|||
|
|||
var allSchemas = new List<ISchemaEntity> { schema }; |
|||
|
|||
A.CallTo(() => appProvider.GetSchemasAsync(appName)).Returns(allSchemas); |
|||
|
|||
sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator()); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(null)] |
|||
[InlineData("")] |
|||
[InlineData(" ")] |
|||
public async Task Should_return_empty_object_for_empty_query(string query) |
|||
{ |
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_multiple_assets_when_querying_assets() |
|||
{ |
|||
const string query = @"
|
|||
query { |
|||
queryAssets(search: ""my-query"", top: 30, skip: 5) { |
|||
id |
|||
version |
|||
created |
|||
createdBy |
|||
lastModified |
|||
lastModifiedBy |
|||
url |
|||
thumbnailUrl |
|||
sourceUrl |
|||
mimeType |
|||
fileName |
|||
fileSize |
|||
fileVersion |
|||
isImage |
|||
pixelWidth |
|||
pixelHeight |
|||
} |
|||
}";
|
|||
|
|||
var asset = CreateAsset(Guid.NewGuid()); |
|||
|
|||
var assets = new List<IAssetEntity> { asset }; |
|||
|
|||
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, null, "my-query", 30, 5)) |
|||
.Returns(assets); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
queryAssets = new dynamic[] |
|||
{ |
|||
new |
|||
{ |
|||
id = asset.Id, |
|||
version = 1, |
|||
created = asset.Created.ToDateTimeUtc(), |
|||
createdBy = "subject:user1", |
|||
lastModified = asset.LastModified.ToDateTimeUtc(), |
|||
lastModifiedBy = "subject:user2", |
|||
url = $"assets/{asset.Id}", |
|||
thumbnailUrl = $"assets/{asset.Id}?width=100", |
|||
sourceUrl = $"assets/source/{asset.Id}", |
|||
mimeType = "image/png", |
|||
fileName = "MyFile.png", |
|||
fileSize = 1024, |
|||
fileVersion = 123, |
|||
isImage = true, |
|||
pixelWidth = 800, |
|||
pixelHeight = 600 |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_single_asset_when_finding_asset() |
|||
{ |
|||
var assetId = Guid.NewGuid(); |
|||
var asset = CreateAsset(Guid.NewGuid()); |
|||
|
|||
var query = $@"
|
|||
query {{ |
|||
findAsset(id: ""{assetId}"") {{ |
|||
id |
|||
version |
|||
created |
|||
createdBy |
|||
lastModified |
|||
lastModifiedBy |
|||
url |
|||
thumbnailUrl |
|||
sourceUrl |
|||
mimeType |
|||
fileName |
|||
fileSize |
|||
fileVersion |
|||
isImage |
|||
pixelWidth |
|||
pixelHeight |
|||
}} |
|||
}}";
|
|||
|
|||
A.CallTo(() => assetRepository.FindAssetAsync(assetId)) |
|||
.Returns(asset); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
findAsset = new |
|||
{ |
|||
id = asset.Id, |
|||
version = 1, |
|||
created = asset.Created.ToDateTimeUtc(), |
|||
createdBy = "subject:user1", |
|||
lastModified = asset.LastModified.ToDateTimeUtc(), |
|||
lastModifiedBy = "subject:user2", |
|||
url = $"assets/{asset.Id}", |
|||
thumbnailUrl = $"assets/{asset.Id}?width=100", |
|||
sourceUrl = $"assets/source/{asset.Id}", |
|||
mimeType = "image/png", |
|||
fileName = "MyFile.png", |
|||
fileSize = 1024, |
|||
fileVersion = 123, |
|||
isImage = true, |
|||
pixelWidth = 800, |
|||
pixelHeight = 600 |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_multiple_contents_when_querying_contents() |
|||
{ |
|||
const string query = @"
|
|||
query { |
|||
queryMySchemaContents(top: 30, skip: 5) { |
|||
id |
|||
version |
|||
created |
|||
createdBy |
|||
lastModified |
|||
lastModifiedBy |
|||
url |
|||
data { |
|||
myString { |
|||
de |
|||
} |
|||
myNumber { |
|||
iv |
|||
} |
|||
myBoolean { |
|||
iv |
|||
} |
|||
myDatetime { |
|||
iv |
|||
} |
|||
myJson { |
|||
iv |
|||
} |
|||
myGeolocation { |
|||
iv |
|||
} |
|||
myTags { |
|||
iv |
|||
} |
|||
} |
|||
} |
|||
}";
|
|||
|
|||
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty); |
|||
|
|||
var contents = new List<IContentEntity> { content }; |
|||
|
|||
A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, "?$top=30&$skip=5")) |
|||
.Returns((schema, 0L, (IReadOnlyList<IContentEntity>)contents)); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
queryMySchemaContents = new dynamic[] |
|||
{ |
|||
new |
|||
{ |
|||
id = content.Id, |
|||
version = 1, |
|||
created = content.Created.ToDateTimeUtc(), |
|||
createdBy = "subject:user1", |
|||
lastModified = content.LastModified.ToDateTimeUtc(), |
|||
lastModifiedBy = "subject:user2", |
|||
url = $"contents/my-schema/{content.Id}", |
|||
data = new |
|||
{ |
|||
myString = new |
|||
{ |
|||
de = "value" |
|||
}, |
|||
myNumber = new |
|||
{ |
|||
iv = 1 |
|||
}, |
|||
myBoolean = new |
|||
{ |
|||
iv = true |
|||
}, |
|||
myDatetime = new |
|||
{ |
|||
iv = content.LastModified.ToDateTimeUtc() |
|||
}, |
|||
myJson = new |
|||
{ |
|||
iv = new |
|||
{ |
|||
value = 1 |
|||
} |
|||
}, |
|||
myGeolocation = new |
|||
{ |
|||
iv = new |
|||
{ |
|||
latitude = 10, |
|||
longitude = 20 |
|||
} |
|||
}, |
|||
myTags = new |
|||
{ |
|||
iv = new[] |
|||
{ |
|||
"tag1", |
|||
"tag2" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_single_content_when_finding_content() |
|||
{ |
|||
var contentId = Guid.NewGuid(); |
|||
var content = CreateContent(contentId, Guid.Empty, Guid.Empty); |
|||
|
|||
var query = $@"
|
|||
query {{ |
|||
findMySchemaContent(id: ""{contentId}"") {{ |
|||
id |
|||
version |
|||
created |
|||
createdBy |
|||
lastModified |
|||
lastModifiedBy |
|||
url |
|||
data {{ |
|||
myString {{ |
|||
de |
|||
}} |
|||
myNumber {{ |
|||
iv |
|||
}} |
|||
myBoolean {{ |
|||
iv |
|||
}} |
|||
myDatetime {{ |
|||
iv |
|||
}} |
|||
myJson {{ |
|||
iv |
|||
}} |
|||
myGeolocation {{ |
|||
iv |
|||
}} |
|||
myTags {{ |
|||
iv |
|||
}} |
|||
}} |
|||
}} |
|||
}}";
|
|||
|
|||
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
|||
.Returns((schema, content)); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
findMySchemaContent = new |
|||
{ |
|||
id = content.Id, |
|||
version = 1, |
|||
created = content.Created.ToDateTimeUtc(), |
|||
createdBy = "subject:user1", |
|||
lastModified = content.LastModified.ToDateTimeUtc(), |
|||
lastModifiedBy = "subject:user2", |
|||
url = $"contents/my-schema/{content.Id}", |
|||
data = new |
|||
{ |
|||
myString = new |
|||
{ |
|||
de = "value" |
|||
}, |
|||
myNumber = new |
|||
{ |
|||
iv = 1 |
|||
}, |
|||
myBoolean = new |
|||
{ |
|||
iv = true |
|||
}, |
|||
myDatetime = new |
|||
{ |
|||
iv = content.LastModified.ToDateTimeUtc() |
|||
}, |
|||
myJson = new |
|||
{ |
|||
iv = new |
|||
{ |
|||
value = 1 |
|||
} |
|||
}, |
|||
myGeolocation = new |
|||
{ |
|||
iv = new |
|||
{ |
|||
latitude = 10, |
|||
longitude = 20 |
|||
} |
|||
}, |
|||
myTags = new |
|||
{ |
|||
iv = new[] |
|||
{ |
|||
"tag1", |
|||
"tag2" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() |
|||
{ |
|||
var contentRefId = Guid.NewGuid(); |
|||
var contentRef = CreateContent(contentRefId, Guid.Empty, Guid.Empty); |
|||
|
|||
var contentId = Guid.NewGuid(); |
|||
var content = CreateContent(contentId, contentRefId, Guid.Empty); |
|||
|
|||
var query = $@"
|
|||
query {{ |
|||
findMySchemaContent(id: ""{contentId}"") {{ |
|||
id |
|||
data {{ |
|||
myReferences {{ |
|||
iv {{ |
|||
id |
|||
}} |
|||
}} |
|||
}} |
|||
}} |
|||
}}";
|
|||
|
|||
var refContents = new List<IContentEntity> { contentRef }; |
|||
|
|||
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
|||
.Returns((schema, content)); |
|||
|
|||
A.CallTo(() => contentQuery.QueryWithCountAsync(app, schema.Id.ToString(), user, false, A<HashSet<Guid>>.That.Matches(x => x.Contains(contentRefId)))) |
|||
.Returns((schema, 0L, (IReadOnlyList<IContentEntity>)refContents)); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
findMySchemaContent = new |
|||
{ |
|||
id = content.Id, |
|||
data = new |
|||
{ |
|||
myReferences = new |
|||
{ |
|||
iv = new[] |
|||
{ |
|||
new |
|||
{ |
|||
id = contentRefId |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query() |
|||
{ |
|||
var assetRefId = Guid.NewGuid(); |
|||
var assetRef = CreateAsset(assetRefId); |
|||
|
|||
var contentId = Guid.NewGuid(); |
|||
var content = CreateContent(contentId, Guid.Empty, assetRefId); |
|||
|
|||
var query = $@"
|
|||
query {{ |
|||
findMySchemaContent(id: ""{contentId}"") {{ |
|||
id |
|||
data {{ |
|||
myAssets {{ |
|||
iv {{ |
|||
id |
|||
}} |
|||
}} |
|||
}} |
|||
}} |
|||
}}";
|
|||
|
|||
var refAssets = new List<IAssetEntity> { assetRef }; |
|||
|
|||
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
|||
.Returns((schema, content)); |
|||
|
|||
A.CallTo(() => assetRepository.QueryAsync(app.Id, null, A<HashSet<Guid>>.That.Matches(x => x.Contains(assetRefId)), null, int.MaxValue, 0)) |
|||
.Returns(refAssets); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = new |
|||
{ |
|||
findMySchemaContent = new |
|||
{ |
|||
id = content.Id, |
|||
data = new |
|||
{ |
|||
myAssets = new |
|||
{ |
|||
iv = new[] |
|||
{ |
|||
new |
|||
{ |
|||
id = assetRefId |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_return_data_when_field_not_part_of_content() |
|||
{ |
|||
var contentId = Guid.NewGuid(); |
|||
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData()); |
|||
|
|||
var query = $@"
|
|||
query {{ |
|||
findMySchemaContent(id: ""{contentId}"") {{ |
|||
id |
|||
version |
|||
created |
|||
createdBy |
|||
lastModified |
|||
lastModifiedBy |
|||
url |
|||
data {{ |
|||
myInvalid {{ |
|||
iv |
|||
}} |
|||
}} |
|||
}} |
|||
}}";
|
|||
|
|||
A.CallTo(() => contentQuery.FindContentAsync(app, schema.Id.ToString(), user, contentId)) |
|||
.Returns((schema, content)); |
|||
|
|||
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query }); |
|||
|
|||
var expected = new |
|||
{ |
|||
data = (object)null |
|||
}; |
|||
|
|||
AssertJson(expected, new { data = result.Data }); |
|||
} |
|||
|
|||
private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null) |
|||
{ |
|||
var now = DateTime.UtcNow.ToInstant(); |
|||
|
|||
data = data ?? |
|||
new NamedContentData() |
|||
.AddField("my-json", |
|||
new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))) |
|||
.AddField("my-string", |
|||
new ContentFieldData().AddValue("de", "value")) |
|||
.AddField("my-assets", |
|||
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) |
|||
.AddField("my-number", |
|||
new ContentFieldData().AddValue("iv", 1)) |
|||
.AddField("my-boolean", |
|||
new ContentFieldData().AddValue("iv", true)) |
|||
.AddField("my-datetime", |
|||
new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) |
|||
.AddField("my-tags", |
|||
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) |
|||
.AddField("my-references", |
|||
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) |
|||
.AddField("my-geolocation", |
|||
new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); |
|||
|
|||
var content = new FakeContentEntity |
|||
{ |
|||
Id = id, |
|||
Version = 1, |
|||
Created = now, |
|||
CreatedBy = new RefToken("subject", "user1"), |
|||
LastModified = now, |
|||
LastModifiedBy = new RefToken("subject", "user2"), |
|||
Data = data |
|||
}; |
|||
|
|||
return content; |
|||
} |
|||
|
|||
private static IAssetEntity CreateAsset(Guid id) |
|||
{ |
|||
var now = DateTime.UtcNow.ToInstant(); |
|||
|
|||
var asset = new FakeAssetEntity |
|||
{ |
|||
Id = id, |
|||
Version = 1, |
|||
Created = now, |
|||
CreatedBy = new RefToken("subject", "user1"), |
|||
LastModified = now, |
|||
LastModifiedBy = new RefToken("subject", "user2"), |
|||
FileName = "MyFile.png", |
|||
FileSize = 1024, |
|||
FileVersion = 123, |
|||
MimeType = "image/png", |
|||
IsImage = true, |
|||
PixelWidth = 800, |
|||
PixelHeight = 600 |
|||
}; |
|||
|
|||
return asset; |
|||
} |
|||
|
|||
private static void AssertJson(object expected, object result) |
|||
{ |
|||
var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented); |
|||
var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented); |
|||
|
|||
Assert.Equal(expectJson, resultJson); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,394 @@ |
|||
// ==========================================================================
|
|||
// ODataQueryTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
/* |
|||
using System; |
|||
using System.Collections.Immutable; |
|||
using FakeItEasy; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.OData.Edm; |
|||
using MongoDB.Bson.Serialization; |
|||
using MongoDB.Driver; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Domain.Apps.Core.Schemas; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Contents.Edm; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents; |
|||
using Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.MongoDb; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public class ODataQueryTests |
|||
{ |
|||
private readonly Schema schemaDef; |
|||
private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; |
|||
private readonly IBsonSerializer<MongoContentEntity> serializer = BsonSerializer.SerializerRegistry.GetSerializer<MongoContentEntity>(); |
|||
private readonly IEdmModel edmModel; |
|||
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); |
|||
|
|||
static ODataQueryTests() |
|||
{ |
|||
InstantSerializer.Register(); |
|||
} |
|||
|
|||
public ODataQueryTests() |
|||
{ |
|||
schemaDef = |
|||
new Schema("user") |
|||
.AddField(new StringField(1, "firstName", Partitioning.Language, |
|||
new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = ImmutableList.Create("1", "2") })) |
|||
.AddField(new StringField(2, "lastName", Partitioning.Language, |
|||
new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })) |
|||
.AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, |
|||
new BooleanFieldProperties())) |
|||
.AddField(new NumberField(4, "age", Partitioning.Invariant, |
|||
new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) |
|||
.AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, |
|||
new DateTimeFieldProperties())) |
|||
.AddField(new AssetsField(6, "pictures", Partitioning.Invariant, |
|||
new AssetsFieldProperties())) |
|||
.AddField(new ReferencesField(7, "friends", Partitioning.Invariant, |
|||
new ReferencesFieldProperties())) |
|||
.AddField(new StringField(8, "dashed-field", Partitioning.Invariant, |
|||
new StringFieldProperties())) |
|||
.Update(new SchemaProperties { Hints = "The User" }); |
|||
|
|||
var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); |
|||
|
|||
var schema = A.Dummy<ISchemaEntity>(); |
|||
A.CallTo(() => schema.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => schema.Version).Returns(3); |
|||
A.CallTo(() => schema.SchemaDef).Returns(schemaDef); |
|||
|
|||
var app = A.Dummy<IAppEntity>(); |
|||
A.CallTo(() => app.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => app.Version).Returns(3); |
|||
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); |
|||
|
|||
edmModel = builder.BuildEdmModel(schema, app); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_parse_query() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); |
|||
|
|||
Assert.NotNull(parser); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_make_query_with_underscore_field() |
|||
{ |
|||
var i = F("$filter=data/dashed_field/iv eq 'Value'"); |
|||
var o = C("{ 'do.8.iv' : 'Value' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_not_operator() |
|||
{ |
|||
var i = F("$filter=not endswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian$/i } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_starts_with_query() |
|||
{ |
|||
var i = F("$filter=startswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /^Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_ends_with_query() |
|||
{ |
|||
var i = F("$filter=endswith(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /Sebastian$/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_contains_query() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian')"); |
|||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_contains_query_with_equals() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true"); |
|||
var o = C("{ 'do.1.de' : /Sebastian/i }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_negated_contains_query_with_equals() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_negated_contains_query_and_other() |
|||
{ |
|||
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true"); |
|||
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_string_equals_query() |
|||
{ |
|||
var i = F("$filter=data/firstName/de eq 'Sebastian'"); |
|||
var o = C("{ 'do.1.de' : 'Sebastian' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_datetime_equals_query() |
|||
{ |
|||
var i = F("$filter=data/birthday/iv eq 1988-01-19T12:00:00Z"); |
|||
var o = C("{ 'do.5.iv' : ISODate(\"1988-01-19T12:00:00Z\") }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_boolean_equals_query() |
|||
{ |
|||
var i = F("$filter=data/isAdmin/iv eq true"); |
|||
var o = C("{ 'do.3.iv' : true }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_string_not_equals_query() |
|||
{ |
|||
var i = F("$filter=data/firstName/de ne 'Sebastian'"); |
|||
var o = C("{ '$or' : [{ 'do.1.de' : { '$exists' : false } }, { 'do.1.de' : { '$ne' : 'Sebastian' } }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_number_less_than_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv lt 1"); |
|||
var o = C("{ 'do.4.iv' : { '$lt' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_number_less_equals_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv le 1"); |
|||
var o = C("{ 'do.4.iv' : { '$lte' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_number_greater_than_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv gt 1"); |
|||
var o = C("{ 'do.4.iv' : { '$gt' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_number_greater_equals_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv ge 1"); |
|||
var o = C("{ 'do.4.iv' : { '$gte' : 1.0 } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_equals_query_for_assets() |
|||
{ |
|||
var i = F("$filter=data/pictures/iv eq 'guid'"); |
|||
var o = C("{ 'do.6.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_equals_query_for_references() |
|||
{ |
|||
var i = F("$filter=data/friends/iv eq 'guid'"); |
|||
var o = C("{ 'do.7.iv' : 'guid' }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_and_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv eq 1 and data/age/iv eq 2"); |
|||
var o = C("{ '$and' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_or_query() |
|||
{ |
|||
var i = F("$filter=data/age/iv eq 1 or data/age/iv eq 2"); |
|||
var o = C("{ '$or' : [{ 'do.4.iv' : 1.0 }, { 'do.4.iv' : 2.0 }] }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_full_text_query() |
|||
{ |
|||
var i = F("$search=Hello my World"); |
|||
var o = C("{ '$text' : { '$search' : 'Hello my World' } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_create_full_text_query_with_and() |
|||
{ |
|||
var i = F("$search=A and B"); |
|||
var o = C("{ '$text' : { '$search' : 'A and B' } }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_convert_orderby_with_single_statements() |
|||
{ |
|||
var i = S("$orderby=data/age/iv desc"); |
|||
var o = C("{ 'do.4.iv' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_convert_orderby_with_multiple_statements() |
|||
{ |
|||
var i = S("$orderby=data/age/iv, data/firstName/en desc"); |
|||
var o = C("{ 'do.4.iv' : 1, 'do.1.en' : -1 }"); |
|||
|
|||
Assert.Equal(o, i); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_top() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$top=3"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.Take(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_max_top_if_larger() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$top=300"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.Take(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(200)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_default_top() |
|||
{ |
|||
var parser = edmModel.ParseQuery(string.Empty); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.Take(parser); |
|||
|
|||
A.CallTo(() => cursor.Limit(20)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_set_skip() |
|||
{ |
|||
var parser = edmModel.ParseQuery("$skip=3"); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.Skip(parser); |
|||
|
|||
A.CallTo(() => cursor.Skip(3)).MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_not_set_skip() |
|||
{ |
|||
var parser = edmModel.ParseQuery(string.Empty); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
cursor.Take(parser); |
|||
|
|||
A.CallTo(() => cursor.Skip(A<int>.Ignored)).MustNotHaveHappened(); |
|||
} |
|||
|
|||
private static string C(string value) |
|||
{ |
|||
return value.Replace('\'', '"'); |
|||
} |
|||
|
|||
private string S(string value) |
|||
{ |
|||
var parser = edmModel.ParseQuery(value); |
|||
var cursor = A.Fake<IFindFluent<MongoContentEntity, MongoContentEntity>>(); |
|||
|
|||
var i = string.Empty; |
|||
|
|||
A.CallTo(() => cursor.Sort(A<SortDefinition<MongoContentEntity>>.Ignored)) |
|||
.Invokes((SortDefinition<MongoContentEntity> sortDefinition) => |
|||
{ |
|||
i = sortDefinition.Render(serializer, registry).ToString(); |
|||
}); |
|||
|
|||
cursor.Sort(parser, schemaDef); |
|||
|
|||
return i; |
|||
} |
|||
|
|||
private string F(string value) |
|||
{ |
|||
var parser = edmModel.ParseQuery(value); |
|||
|
|||
var query = FilterBuilder.Build(parser, schemaDef).Render(serializer, registry).ToString(); |
|||
|
|||
return query; |
|||
} |
|||
} |
|||
} |
|||
*/ |
|||
@ -0,0 +1,50 @@ |
|||
// ==========================================================================
|
|||
// MockupAssetEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
|||
{ |
|||
public sealed class FakeAssetEntity : IAssetEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public Guid AppId { get; set; } |
|||
|
|||
public Guid AssetId { get; set; } |
|||
|
|||
public Instant Created { get; set; } |
|||
|
|||
public Instant LastModified { get; set; } |
|||
|
|||
public RefToken CreatedBy { get; set; } |
|||
|
|||
public RefToken LastModifiedBy { get; set; } |
|||
|
|||
public long Version { get; set; } |
|||
|
|||
public string MimeType { get; set; } |
|||
|
|||
public string FileName { get; set; } |
|||
|
|||
public long FileSize { get; set; } |
|||
|
|||
public long FileVersion { get; set; } |
|||
|
|||
public bool IsImage { get; set; } |
|||
|
|||
public bool IsDeleted { get; set; } |
|||
|
|||
public int? PixelWidth { get; set; } |
|||
|
|||
public int? PixelHeight { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
// ==========================================================================
|
|||
// FakeContentEntity.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
|||
{ |
|||
public sealed class FakeContentEntity : IContentEntity |
|||
{ |
|||
public Guid Id { get; set; } |
|||
|
|||
public Guid AppId { get; set; } |
|||
|
|||
public long Version { get; set; } |
|||
|
|||
public Instant Created { get; set; } |
|||
|
|||
public Instant LastModified { get; set; } |
|||
|
|||
public RefToken CreatedBy { get; set; } |
|||
|
|||
public RefToken LastModifiedBy { get; set; } |
|||
|
|||
public NamedContentData Data { get; set; } |
|||
|
|||
public Status Status { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// ==========================================================================
|
|||
// FakeUrlGenerator.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Contents.GraphQL; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.TestData |
|||
{ |
|||
public sealed class FakeUrlGenerator : IGraphQLUrlGenerator |
|||
{ |
|||
public bool CanGenerateAssetSourceUrl { get; } = true; |
|||
|
|||
public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset) |
|||
{ |
|||
return $"assets/{asset.Id}"; |
|||
} |
|||
|
|||
public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset) |
|||
{ |
|||
return $"assets/{asset.Id}?width=100"; |
|||
} |
|||
|
|||
public string GenerateAssetSourceUrl(IAppEntity app, IAssetEntity asset) |
|||
{ |
|||
return $"assets/source/{asset.Id}"; |
|||
} |
|||
|
|||
public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content) |
|||
{ |
|||
return $"contents/{schema.Name}/{content.Id}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,99 @@ |
|||
// ==========================================================================
|
|||
// RuleDequeuerGrainTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
|||
using Squidex.Infrastructure.Log; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules |
|||
{ |
|||
public class RuleDequeuerTests |
|||
{ |
|||
private readonly IClock clock = A.Fake<IClock>(); |
|||
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); |
|||
private readonly RuleService ruleService = A.Fake<RuleService>(); |
|||
private readonly RuleDequeuer sut; |
|||
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
|||
|
|||
public RuleDequeuerTests() |
|||
{ |
|||
A.CallTo(() => clock.GetCurrentInstant()).Returns(now); |
|||
|
|||
sut = new RuleDequeuer( |
|||
ruleService, |
|||
ruleEventRepository, |
|||
log, |
|||
clock); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(0, 0, RuleResult.Success, RuleJobResult.Success)] |
|||
[InlineData(0, 5, RuleResult.Timeout, RuleJobResult.Retry)] |
|||
[InlineData(1, 60, RuleResult.Timeout, RuleJobResult.Retry)] |
|||
[InlineData(2, 360, RuleResult.Failed, RuleJobResult.Retry)] |
|||
[InlineData(3, 720, RuleResult.Failed, RuleJobResult.Retry)] |
|||
[InlineData(4, 0, RuleResult.Failed, RuleJobResult.Failed)] |
|||
public async Task Should_set_next_attempt_based_on_num_calls(int calls, int minutes, RuleResult result, RuleJobResult jobResult) |
|||
{ |
|||
var actionData = new RuleJobData(); |
|||
var actionName = "MyAction"; |
|||
|
|||
var @event = CreateEvent(calls, actionName, actionData); |
|||
|
|||
var requestElapsed = TimeSpan.FromMinutes(1); |
|||
var requestDump = "Dump"; |
|||
|
|||
A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) |
|||
.Returns((requestDump, result, requestElapsed)); |
|||
|
|||
Instant? nextCall = null; |
|||
|
|||
if (minutes > 0) |
|||
{ |
|||
nextCall = now.Plus(Duration.FromMinutes(minutes)); |
|||
} |
|||
|
|||
await sut.HandleAsync(@event); |
|||
|
|||
sut.Dispose(); |
|||
|
|||
A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, requestDump, result, jobResult, requestElapsed, nextCall)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
private IRuleEventEntity CreateEvent(int numCalls, string actionName, RuleJobData actionData) |
|||
{ |
|||
var @event = A.Fake<IRuleEventEntity>(); |
|||
|
|||
var job = new RuleJob |
|||
{ |
|||
RuleId = Guid.NewGuid(), |
|||
ActionData = actionData, |
|||
ActionName = actionName, |
|||
Created = now |
|||
}; |
|||
|
|||
A.CallTo(() => @event.Id).Returns(Guid.NewGuid()); |
|||
A.CallTo(() => @event.Job).Returns(job); |
|||
A.CallTo(() => @event.Created).Returns(now); |
|||
A.CallTo(() => @event.NumCalls).Returns(numCalls); |
|||
|
|||
return @event; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
// ==========================================================================
|
|||
// RuleEnqueuerTests.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using FakeItEasy; |
|||
using NodaTime; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
using Squidex.Domain.Apps.Core.Rules.Actions; |
|||
using Squidex.Domain.Apps.Core.Rules.Triggers; |
|||
using Squidex.Domain.Apps.Entities.Rules.Repositories; |
|||
using Squidex.Domain.Apps.Events.Contents; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Rules |
|||
{ |
|||
public class RuleEnqueuerTests |
|||
{ |
|||
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); |
|||
private readonly IRuleEventRepository ruleEventRepository = A.Fake<IRuleEventRepository>(); |
|||
private readonly RuleService ruleService = A.Fake<RuleService>(); |
|||
private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); |
|||
private readonly NamedId<Guid> appId = new NamedId<Guid>(Guid.NewGuid(), "my-app"); |
|||
private readonly RuleEnqueuer sut; |
|||
|
|||
public RuleEnqueuerTests() |
|||
{ |
|||
sut = new RuleEnqueuer( |
|||
ruleEventRepository, |
|||
appProvider, |
|||
ruleService); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_contents_filter_for_events_filter() |
|||
{ |
|||
Assert.Equal(".*", sut.EventsFilter); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_return_type_name_for_name() |
|||
{ |
|||
Assert.Equal(typeof(RuleEnqueuer).Name, sut.Name); |
|||
} |
|||
|
|||
[Fact] |
|||
public Task Should_do_nothing_on_clear() |
|||
{ |
|||
return sut.ClearAsync(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_update_repositories_on_with_jobs_from_sender() |
|||
{ |
|||
var @event = Envelope.Create(new ContentCreated { AppId = appId }); |
|||
|
|||
var rule1 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
|||
var rule2 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
|||
var rule3 = new Rule(new ContentChangedTrigger(), new WebhookAction { Url = new Uri("https://squidex.io") }); |
|||
|
|||
var job1 = new RuleJob { Created = now }; |
|||
var job2 = new RuleJob { Created = now }; |
|||
|
|||
var ruleEntity1 = A.Fake<IRuleEntity>(); |
|||
var ruleEntity2 = A.Fake<IRuleEntity>(); |
|||
var ruleEntity3 = A.Fake<IRuleEntity>(); |
|||
|
|||
A.CallTo(() => ruleEntity1.RuleDef).Returns(rule1); |
|||
A.CallTo(() => ruleEntity2.RuleDef).Returns(rule2); |
|||
A.CallTo(() => ruleEntity3.RuleDef).Returns(rule3); |
|||
|
|||
A.CallTo(() => appProvider.GetRulesAsync(appId.Name)) |
|||
.Returns(new List<IRuleEntity> { ruleEntity1, ruleEntity2, ruleEntity3 }); |
|||
|
|||
A.CallTo(() => ruleService.CreateJob(rule1, @event)) |
|||
.Returns(job1); |
|||
|
|||
A.CallTo(() => ruleService.CreateJob(rule2, @event)) |
|||
.Returns(job2); |
|||
|
|||
A.CallTo(() => ruleService.CreateJob(rule3, @event)) |
|||
.Returns(null); |
|||
|
|||
await sut.On(@event); |
|||
|
|||
A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, now)) |
|||
.MustHaveHappened(); |
|||
|
|||
A.CallTo(() => ruleEventRepository.EnqueueAsync(job2, now)) |
|||
.MustHaveHappened(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>Exe</OutputType> |
|||
<TargetFramework>netcoreapp2.0</TargetFramework> |
|||
<RootNamespace>Squidex.Domain.Apps.Entities</RootNamespace> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Compile Remove="MongoDb\**" /> |
|||
<EmbeddedResource Remove="MongoDb\**" /> |
|||
<None Remove="MongoDb\**" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj" /> |
|||
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" /> |
|||
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" /> |
|||
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
|||
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<PackageReference Include="FakeItEasy" Version="4.2.0" /> |
|||
<PackageReference Include="FluentAssertions" Version="4.19.4" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" /> |
|||
<PackageReference Include="MongoDB.Driver" Version="2.4.4" /> |
|||
<PackageReference Include="RefactoringEssentials" Version="5.4.0" /> |
|||
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> |
|||
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> |
|||
<PackageReference Include="xunit" Version="2.3.1" /> |
|||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> |
|||
</ItemGroup> |
|||
<PropertyGroup> |
|||
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -0,0 +1,45 @@ |
|||
// ==========================================================================
|
|||
// AssertHelper.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using FluentAssertions; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.TestHelpers |
|||
{ |
|||
public static class AssertHelper |
|||
{ |
|||
public static void ShouldHaveSameEvents(this IEnumerable<Envelope<IEvent>> events, params IEvent[] others) |
|||
{ |
|||
var source = events.Select(x => x.Payload).ToArray(); |
|||
|
|||
source.Should().HaveSameCount(others); |
|||
|
|||
for (var i = 0; i < source.Length; i++) |
|||
{ |
|||
var lhs = source[i]; |
|||
var rhs = others[i]; |
|||
|
|||
lhs.ShouldBeSameEvent(rhs); |
|||
} |
|||
} |
|||
|
|||
public static void ShouldBeSameEvent(this IEvent lhs, IEvent rhs) |
|||
{ |
|||
lhs.Should().BeOfType(rhs.GetType()); |
|||
|
|||
((object)lhs).ShouldBeEquivalentTo(rhs, o => o.IncludingAllDeclaredProperties()); |
|||
} |
|||
|
|||
public static void ShouldBeSameEventType(this IEvent lhs, IEvent rhs) |
|||
{ |
|||
lhs.Should().BeOfType(rhs.GetType()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,162 @@ |
|||
// ==========================================================================
|
|||
// HandlerTestBase.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Events; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
|
|||
#pragma warning disable IDE0019 // Use pattern matching
|
|||
|
|||
namespace Squidex.Domain.Apps.Entities.TestHelpers |
|||
{ |
|||
public abstract class HandlerTestBase<T> where T : IDomainObject |
|||
{ |
|||
private sealed class MockupHandler : IAggregateHandler |
|||
{ |
|||
private T domainObject; |
|||
|
|||
public bool IsCreated { get; private set; } |
|||
public bool IsUpdated { get; private set; } |
|||
|
|||
public void Init(T newDomainObject) |
|||
{ |
|||
domainObject = newDomainObject; |
|||
|
|||
IsCreated = false; |
|||
IsUpdated = false; |
|||
} |
|||
|
|||
public async Task<V> CreateAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IDomainObject |
|||
{ |
|||
IsCreated = true; |
|||
|
|||
var @do = domainObject as V; |
|||
|
|||
await creator(domainObject as V); |
|||
|
|||
return @do; |
|||
} |
|||
|
|||
public async Task<V> UpdateAsync<V>(CommandContext context, Func<V, Task> updater) where V : class, IDomainObject |
|||
{ |
|||
IsUpdated = true; |
|||
|
|||
var @do = domainObject as V; |
|||
|
|||
await updater(domainObject as V); |
|||
|
|||
return @do; |
|||
} |
|||
} |
|||
|
|||
private readonly MockupHandler handler = new MockupHandler(); |
|||
|
|||
protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString()); |
|||
|
|||
protected Guid AppId { get; } = Guid.NewGuid(); |
|||
|
|||
protected Guid SchemaId { get; } = Guid.NewGuid(); |
|||
|
|||
protected string AppName { get; } = "my-app"; |
|||
|
|||
protected string SchemaName { get; } = "my-schema"; |
|||
|
|||
protected NamedId<Guid> AppNamedId |
|||
{ |
|||
get { return new NamedId<Guid>(AppId, AppName); } |
|||
} |
|||
|
|||
protected NamedId<Guid> SchemaNamedId |
|||
{ |
|||
get { return new NamedId<Guid>(SchemaId, SchemaName); } |
|||
} |
|||
|
|||
protected IAggregateHandler Handler |
|||
{ |
|||
get { return handler; } |
|||
} |
|||
|
|||
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : SquidexCommand |
|||
{ |
|||
return new CommandContext(CreateCommand(command)); |
|||
} |
|||
|
|||
protected async Task TestCreate(T domainObject, Func<T, Task> action, bool shouldCreate = true) |
|||
{ |
|||
handler.Init(domainObject); |
|||
|
|||
await action(domainObject); |
|||
|
|||
if (!handler.IsCreated && shouldCreate) |
|||
{ |
|||
throw new InvalidOperationException("Create not called."); |
|||
} |
|||
} |
|||
|
|||
protected async Task TestUpdate(T domainObject, Func<T, Task> action, bool shouldUpdate = true) |
|||
{ |
|||
handler.Init(domainObject); |
|||
|
|||
await action(domainObject); |
|||
|
|||
if (!handler.IsUpdated && shouldUpdate) |
|||
{ |
|||
throw new InvalidOperationException("Update not called."); |
|||
} |
|||
} |
|||
|
|||
protected TCommand CreateCommand<TCommand>(TCommand command) where TCommand : SquidexCommand |
|||
{ |
|||
if (command.Actor == null) |
|||
{ |
|||
command.Actor = User; |
|||
} |
|||
|
|||
var appCommand = command as AppCommand; |
|||
|
|||
if (appCommand != null && appCommand.AppId == null) |
|||
{ |
|||
appCommand.AppId = AppNamedId; |
|||
} |
|||
|
|||
var schemaCommand = command as SchemaCommand; |
|||
|
|||
if (schemaCommand != null && schemaCommand.SchemaId == null) |
|||
{ |
|||
schemaCommand.SchemaId = SchemaNamedId; |
|||
} |
|||
|
|||
return command; |
|||
} |
|||
|
|||
protected TEvent CreateEvent<TEvent>(TEvent @event) where TEvent : SquidexEvent |
|||
{ |
|||
@event.Actor = User; |
|||
|
|||
var appEvent = @event as AppEvent; |
|||
|
|||
if (appEvent != null) |
|||
{ |
|||
appEvent.AppId = AppNamedId; |
|||
} |
|||
|
|||
var schemaEvent = @event as SchemaEvent; |
|||
|
|||
if (schemaEvent != null) |
|||
{ |
|||
schemaEvent.SchemaId = SchemaNamedId; |
|||
} |
|||
|
|||
return @event; |
|||
} |
|||
} |
|||
} |
|||
|
|||
#pragma warning restore IDE0019 // Use pattern matching
|
|||
Loading…
Reference in new issue