Browse Source

Tests for read models and apps.

pull/206/head
Sebastian Stehle 9 years ago
parent
commit
cd264df6c8
  1. 15
      Squidex.sln
  2. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  3. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  4. 5
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  5. 30
      src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs
  6. 49
      src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
  7. 216
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  8. 74
      src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
  9. 39
      src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs
  10. 85
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  11. 229
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  12. 23
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs
  13. 67
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs
  14. 38
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs
  15. 19
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs
  16. 27
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs
  17. 170
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  18. 68
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  19. 112
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  20. 192
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs
  21. 37
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs
  22. 25
      src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs
  23. 28
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  24. 146
      src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs
  25. 33
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  26. 1
      src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs
  27. 1
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs
  28. 3
      src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj
  29. 1
      src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs
  30. 14
      src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  31. 244
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs
  32. 284
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  33. 51
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs
  34. 159
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs
  35. 142
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs
  36. 158
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  37. 131
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs
  38. 118
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs
  39. 38
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs
  40. 220
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  41. 689
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs
  42. 394
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs
  43. 50
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
  44. 36
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs
  45. 40
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs
  46. 99
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs
  47. 102
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  48. 36
      tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  49. 45
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs
  50. 162
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

15
Squidex.sln

@ -67,6 +67,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchma
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities", "src\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj", "{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entities.Tests", "tests\Squidex.Domain.Apps.Entities.Tests\Squidex.Domain.Apps.Entities.Tests.csproj", "{AA003372-CD8D-4DBC-962C-F61E0C93CF05}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -329,6 +331,18 @@ Global
{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x64.Build.0 = Release|Any CPU
{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.ActiveCfg = Release|Any CPU
{79FEF326-CA5E-4698-B2BA-C16A4580B4D5}.Release|x86.Build.0 = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x64.ActiveCfg = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x64.Build.0 = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x86.ActiveCfg = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Debug|x86.Build.0 = Debug|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|Any CPU.Build.0 = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.ActiveCfg = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x64.Build.0 = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.ActiveCfg = Release|Any CPU
{AA003372-CD8D-4DBC-962C-F61E0C93CF05}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -359,6 +373,7 @@ Global
{F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892}
{6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892}
{79FEF326-CA5E-4698-B2BA-C16A4580B4D5} = {C9809D59-6665-471E-AD87-5AC624C65892}
{AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}

2
src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs

@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
return handler.UpdateAsync<AppDomainObject>(context, async a =>
{
await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan.PlanId));
await GuardAppContributors.CanAssign(a.State.Contributors, command, userResolver, appPlansProvider.GetPlan(a.State.Plan?.PlanId));
a.AssignContributor(command);
});

6
src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs

@ -28,7 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
var appId = new NamedId<Guid>(command.AppId, command.Name);
UpdateState(command, s => s.Name = command.Name);
UpdateState(command, s => { s.Id = appId.Id; s.Name = command.Name; });
UpdateContributors(command, c => c.Assign(command.Actor.Identifier, AppContributorPermission.Owner));
RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId)));
RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command)));
@ -158,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
ThrowIfNotCreated();
UpdateState(command, s => s.Plan = new AppPlan(command.Actor, command.PlanId));
UpdateState(command, s => s.Plan = command.PlanId != null ? new AppPlan(command.Actor, command.PlanId) : null);
RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged()));

5
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -8,11 +8,14 @@
using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.State
{
public sealed class AppState : DomainObjectState<AppState>, IAppEntity
{
private static readonly LanguagesConfig English = LanguagesConfig.Build(Language.EN);
[JsonProperty]
public string Name { get; set; }
@ -26,6 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
public AppContributors Contributors { get; set; } = AppContributors.Empty;
[JsonProperty]
public LanguagesConfig LanguagesConfig { get; set; }
public LanguagesConfig LanguagesConfig { get; set; } = English;
}
}

30
src/Squidex.Domain.Apps.Entities/CachingProviderBase.cs

@ -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;
}
}
}

49
src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs

@ -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);
}
}
}

216
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -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; }
}
}
}

74
src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs

@ -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;
}
}
}

39
src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelExtensions.cs

@ -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;
}
}
}

85
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -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}";
}
}
}

229
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -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;
}
}
}

23
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQuery.cs

@ -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; }
}
}

67
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs

@ -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>();
}
}
}
}

38
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs

@ -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);
}
}

19
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs

@ -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);
}
}

27
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs

@ -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);
}
}

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

@ -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));
}
}
}

68
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -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.";
}
}
}

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

@ -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));
}
}
}

192
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentQueryGraphType.cs

@ -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;
}
}
}

37
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NoopGraphType.cs

@ -0,0 +1,37 @@
// ==========================================================================
// NoopGraphType.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using GraphQL.Language.AST;
using GraphQL.Types;
namespace Squidex.Domain.Apps.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();
}
}
}

25
src/Squidex.Domain.Apps.Entities/Contents/IContentEntity.cs

@ -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; }
}
}

28
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -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);
}
}

146
src/Squidex.Domain.Apps.Entities/Contents/QueryContext.cs

@ -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();
}
}
}

33
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -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);
}
}

1
src/Squidex.Domain.Apps.Entities/Rules/Guards/GuardRule.cs

@ -8,7 +8,6 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;

1
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs

@ -12,7 +12,6 @@ using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;

3
src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj

@ -11,9 +11,12 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="GraphQL" Version="0.17.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.0.0" />
<PackageReference Include="NodaTime" Version="2.2.3" />
<PackageReference Include="RefactoringEssentials" Version="5.4.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.8.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup>
<PropertyGroup>

1
src/Squidex.Infrastructure.GetEventStore/EventSourcing/Events/GetEventStore.cs

@ -12,7 +12,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EventStore.ClientAPI;
using EventStore.ClientAPI.Exceptions;
namespace Squidex.Infrastructure.EventSourcing
{

14
src/Squidex.Infrastructure/Commands/DomainObjectBase.cs

@ -15,11 +15,11 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public abstract class DomainObjectBase<TBase, TState> : IDomainObject
public abstract class DomainObjectBase<TBase, TState> : IDomainObject where TState : new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private int version = -1;
private TState state;
private TState state = new TState();
private IPersistence<TState> persistence;
public TState State
@ -32,6 +32,16 @@ namespace Squidex.Infrastructure.Commands
get { return version; }
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
{
return uncomittedEvents;
}
public void ClearUncommittedEvents()
{
uncomittedEvents.Clear();
}
public Task ActivateAsync(string key, IStore store)
{
persistence = store.WithSnapshots<TBase, TState>(key, s => state = s);

244
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs

@ -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")));
}
}
}

284
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs

@ -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();
}
}
}

51
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventTests.cs

@ -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;
}
}
}

159
tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs

@ -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;
}
}
}

142
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs

@ -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);
}
}
}

158
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -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);
}
}
}

131
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppLanguagesTests.cs

@ -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);
}
}
}

118
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppTests.cs

@ -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);
}
}
}

38
tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs

@ -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));
}
}
}

220
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -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);
}
}
}

689
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQLTests.cs

@ -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);
}
}
}

394
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ODataQueryTests.cs

@ -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;
}
}
}
*/

50
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs

@ -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; }
}
}

36
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs

@ -0,0 +1,36 @@
// ==========================================================================
// FakeContentEntity.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.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; }
}
}

40
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs

@ -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}";
}
}
}

99
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs

@ -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;
}
}
}

102
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -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();
}
}
}

36
tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -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>

45
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs

@ -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());
}
}
}

162
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

@ -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…
Cancel
Save