Browse Source

Migrated code from orleans3 branch.

pull/250/head
Sebastian Stehle 8 years ago
parent
commit
58eb548642
  1. 26
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs
  2. 18
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  3. 200
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  4. 228
      src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  5. 314
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  7. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  8. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs
  9. 2
      src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs
  10. 5
      src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs
  11. 5
      src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs
  12. 130
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  13. 89
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  14. 158
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  15. 126
      src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  16. 232
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  17. 89
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  18. 89
      src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs
  19. 86
      src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  20. 129
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  21. 194
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs
  22. 229
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs
  23. 304
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  24. 26
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs
  25. 19
      src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs
  26. 13
      src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  27. 149
      src/Squidex.Infrastructure/Commands/AggregateHandler.cs
  28. 21
      src/Squidex.Infrastructure/Commands/CommandExtensions.cs
  29. 106
      src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  30. 204
      src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  31. 46
      src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs
  32. 23
      src/Squidex.Infrastructure/Commands/IAggregateHandler.cs
  33. 10
      src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs
  34. 63
      src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs
  35. 9
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  36. 2
      src/Squidex.Infrastructure/States/IStatefulObject.cs
  37. 6
      src/Squidex.Infrastructure/States/IStore.cs
  38. 6
      src/Squidex.Infrastructure/States/Persistence.cs
  39. 16
      src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs
  40. 30
      src/Squidex.Infrastructure/States/StateFactory.cs
  41. 18
      src/Squidex.Infrastructure/States/Store.cs
  42. 43
      src/Squidex.Infrastructure/States/StoreExtensions.cs
  43. 38
      src/Squidex.Infrastructure/Tasks/TaskExtensions.cs
  44. 3
      src/Squidex/Config/Domain/InfrastructureServices.cs
  45. 22
      src/Squidex/Config/Domain/WriteServices.cs
  46. 291
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppCommandMiddlewareTests.cs
  47. 399
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  48. 388
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  49. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs
  50. 81
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  51. 220
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  52. 170
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  53. 264
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs
  54. 304
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  55. 361
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
  56. 118
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs
  57. 256
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs
  58. 214
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs
  59. 280
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs
  60. 665
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs
  61. 428
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs
  62. 5
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/AssertHelper.cs
  63. 121
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  64. 284
      tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs
  65. 88
      tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs
  66. 245
      tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs
  67. 64
      tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs
  68. 86
      tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs
  69. 38
      tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs
  70. 186
      tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  71. 171
      tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs
  72. 11
      tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs
  73. 4
      tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs
  74. 15
      tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs
  75. 27
      tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs
  76. 6
      tools/Migrate_01/Migrations/AddPatterns.cs
  77. 8
      tools/Migrate_01/Rebuilder.cs

26
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
@ -27,6 +29,18 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
}
}
public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func<string> message)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidateAsync(data);
if (validator.Errors.Count > 0)
{
throw new ValidationException(message(), validator.Errors.ToList());
}
}
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList<ValidationError> errors)
{
var validator = new ContentValidator(schema, partitionResolver, context);
@ -38,5 +52,17 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
errors.Add(error);
}
}
public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, Func<string> message)
{
var validator = new ContentValidator(schema, partitionResolver, context);
await validator.ValidatePartialAsync(data);
if (validator.Errors.Count > 0)
{
throw new ValidationException(message(), validator.Errors.ToList());
}
}
}
}

18
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -47,14 +47,14 @@ namespace Squidex.Domain.Apps.Entities
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId);
var app = await stateFactory.GetSingleAsync<AppGrain>(appId);
if (!IsFound(app))
{
return (null, null);
}
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id);
var schema = await stateFactory.GetSingleAsync<SchemaGrain>(id);
if (!IsFound(schema) || schema.Snapshot.IsDeleted)
{
@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities
return null;
}
return (await stateFactory.GetSingleAsync<AppDomainObject>(appId)).Snapshot;
return (await stateFactory.GetSingleAsync<AppGrain>(appId)).Snapshot;
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name)
@ -85,12 +85,12 @@ namespace Squidex.Domain.Apps.Entities
return null;
}
return (await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId)).Snapshot;
return (await stateFactory.GetSingleAsync<SchemaGrain>(schemaId)).Snapshot;
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id);
var schema = await stateFactory.GetSingleAsync<SchemaGrain>(id);
if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId)
{
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities
var schemas =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<SchemaDomainObject>(id)));
ids.Select(id => stateFactory.GetSingleAsync<SchemaGrain>(id)));
return schemas.Where(IsFound).Select(s => (ISchemaEntity)s.Snapshot).ToList();
}
@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities
var rules =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<RuleDomainObject>(id)));
ids.Select(id => stateFactory.GetSingleAsync<RuleGrain>(id)));
return rules.Where(IsFound).Select(r => (IRuleEntity)r.Snapshot).ToList();
}
@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities
var apps =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<AppDomainObject>(id)));
ids.Select(id => stateFactory.GetSingleAsync<AppGrain>(id)));
return apps.Where(IsFound).Select(a => (IAppEntity)a.Snapshot).ToList();
}
@ -143,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities
return await schemaRepository.FindSchemaIdAsync(appId, name);
}
private static bool IsFound(IDomainObject app)
private static bool IsFound(IDomainObjectGrain app)
{
return app.Version > EtagVersion.Empty;
}

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

@ -1,200 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppCommandMiddleware : ICommandMiddleware
{
private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider;
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver;
public AppCommandMiddleware(
IAggregateHandler handler,
IAppProvider appProvider,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(appPlansProvider, nameof(appPlansProvider));
Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager));
this.handler = handler;
this.userResolver = userResolver;
this.appProvider = appProvider;
this.appPlansProvider = appPlansProvider;
this.appPlansBillingManager = appPlansBillingManager;
}
protected async Task On(CreateApp command, CommandContext context)
{
var app = await handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
{
await GuardApp.CanCreate(command, appProvider);
a.Create(command);
context.Complete(EntityCreatedResult.Create(command.AppId, a.Version));
});
}
protected Task On(AssignContributor command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, async a =>
{
await GuardAppContributors.CanAssign(a.Snapshot.Contributors, command, userResolver, appPlansProvider.GetPlan(a.Snapshot.Plan?.PlanId));
a.AssignContributor(command);
});
}
protected Task On(RemoveContributor command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppContributors.CanRemove(a.Snapshot.Contributors, command);
a.RemoveContributor(command);
});
}
protected Task On(AttachClient command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanAttach(a.Snapshot.Clients, command);
a.AttachClient(command);
});
}
protected Task On(UpdateClient command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanUpdate(a.Snapshot.Clients, command);
a.UpdateClient(command);
});
}
protected Task On(RevokeClient command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppClients.CanRevoke(a.Snapshot.Clients, command);
a.RevokeClient(command);
});
}
protected Task On(AddLanguage command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanAdd(a.Snapshot.LanguagesConfig, command);
a.AddLanguage(command);
});
}
protected Task On(RemoveLanguage command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanRemove(a.Snapshot.LanguagesConfig, command);
a.RemoveLanguage(command);
});
}
protected Task On(UpdateLanguage command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppLanguages.CanUpdate(a.Snapshot.LanguagesConfig, command);
a.UpdateLanguage(command);
});
}
protected Task On(AddPattern command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanAdd(a.Snapshot.Patterns, command);
a.AddPattern(command);
});
}
protected Task On(DeletePattern command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanDelete(a.Snapshot.Patterns, command);
a.DeletePattern(command);
});
}
protected async Task On(UpdatePattern command, CommandContext context)
{
await handler.UpdateSyncedAsync<AppDomainObject>(context, a =>
{
GuardAppPattern.CanUpdate(a.Snapshot.Patterns, command);
a.UpdatePattern(command);
});
}
protected Task On(ChangePlan command, CommandContext context)
{
return handler.UpdateSyncedAsync<AppDomainObject>(context, async a =>
{
GuardApp.CanChangePlan(command, a.Snapshot.Plan, appPlansProvider);
if (command.FromCallback)
{
a.ChangePlan(command);
}
else
{
var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Snapshot.Id, a.Snapshot.Name, command.PlanId);
if (result is PlanChangedResult)
{
a.ChangePlan(command);
}
context.Complete(result);
}
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await this.DispatchActionAsync(context.Command, context);
await next();
}
}
}

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

@ -1,228 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Apps
{
public sealed class AppDomainObject : SquidexDomainObjectBase<AppState>
{
private readonly InitialPatterns initialPatterns;
public AppDomainObject(InitialPatterns initialPatterns)
{
Guard.NotNull(initialPatterns, nameof(initialPatterns));
this.initialPatterns = initialPatterns;
}
public AppDomainObject Create(CreateApp command)
{
ThrowIfCreated();
var appId = new NamedId<Guid>(command.AppId, command.Name);
var events = new List<AppEvent>
{
CreateInitalEvent(command.Name),
CreateInitialOwner(command.Actor),
CreateInitialLanguage()
};
foreach (var pattern in initialPatterns)
{
events.Add(CreateInitialPattern(pattern.Key, pattern.Value));
}
foreach (var @event in events)
{
@event.Actor = command.Actor;
@event.AppId = appId;
RaiseEvent(@event);
}
return this;
}
public AppDomainObject UpdateLanguage(UpdateLanguage command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated()));
return this;
}
public AppDomainObject UpdateClient(UpdateClient command)
{
ThrowIfNotCreated();
if (!string.IsNullOrWhiteSpace(command.Name))
{
RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed()));
}
if (command.Permission.HasValue)
{
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value }));
}
return this;
}
public AppDomainObject AssignContributor(AssignContributor command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned()));
return this;
}
public AppDomainObject RemoveContributor(RemoveContributor command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved()));
return this;
}
public AppDomainObject AttachClient(AttachClient command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppClientAttached()));
return this;
}
public AppDomainObject RevokeClient(RevokeClient command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked()));
return this;
}
public AppDomainObject AddLanguage(AddLanguage command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded()));
return this;
}
public AppDomainObject RemoveLanguage(RemoveLanguage command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved()));
return this;
}
public AppDomainObject ChangePlan(ChangePlan command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged()));
return this;
}
public AppDomainObject AddPattern(AddPattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded()));
return this;
}
public AppDomainObject DeletePattern(DeletePattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted()));
return this;
}
public AppDomainObject UpdatePattern(UpdatePattern command)
{
ThrowIfNotCreated();
RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated()));
return this;
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = new NamedId<Guid>(Snapshot.Id, Snapshot.Name);
}
RaiseEvent(Envelope.Create(@event));
}
private static AppCreated CreateInitalEvent(string name)
{
return new AppCreated { Name = name };
}
private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern)
{
return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message };
}
private static AppLanguageAdded CreateInitialLanguage()
{
return new AppLanguageAdded { Language = Language.EN };
}
private static AppContributorAssigned CreateInitialOwner(RefToken actor)
{
return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner };
}
private void ThrowIfNotCreated()
{
if (string.IsNullOrWhiteSpace(Snapshot.Name))
{
throw new DomainException("App has not been created.");
}
}
private void ThrowIfCreated()
{
if (!string.IsNullOrWhiteSpace(Snapshot.Name))
{
throw new DomainException("App has already been created.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
}
}

314
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -0,0 +1,314 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Guards;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppGrain : DomainObjectGrain<AppState>
{
private readonly InitialPatterns initialPatterns;
private readonly IAppProvider appProvider;
private readonly IAppPlansProvider appPlansProvider;
private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver;
public AppGrain(
InitialPatterns initialPatterns,
IStore<Guid> store,
IAppProvider appProvider,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)
: base(store)
{
Guard.NotNull(initialPatterns, nameof(initialPatterns));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(appPlansProvider, nameof(appPlansProvider));
Guard.NotNull(appPlansBillingManager, nameof(appPlansBillingManager));
this.userResolver = userResolver;
this.appProvider = appProvider;
this.appPlansProvider = appPlansProvider;
this.appPlansBillingManager = appPlansBillingManager;
this.initialPatterns = initialPatterns;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case CreateApp createApp:
return CreateAsync(createApp, async c =>
{
await GuardApp.CanCreate(c, appProvider);
Create(c);
});
case AssignContributor assigneContributor:
return UpdateAsync(assigneContributor, async c =>
{
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
AssignContributor(c);
});
case RemoveContributor removeContributor:
return UpdateAsync(removeContributor, c =>
{
GuardAppContributors.CanRemove(Snapshot.Contributors, c);
RemoveContributor(c);
});
case AttachClient attachClient:
return UpdateAsync(attachClient, c =>
{
GuardAppClients.CanAttach(Snapshot.Clients, c);
AttachClient(c);
});
case UpdateClient updateClient:
return UpdateAsync(updateClient, c =>
{
GuardAppClients.CanUpdate(Snapshot.Clients, c);
UpdateClient(c);
});
case RevokeClient revokeClient:
return UpdateAsync(revokeClient, c =>
{
GuardAppClients.CanRevoke(Snapshot.Clients, c);
RevokeClient(c);
});
case AddLanguage addLanguage:
return UpdateAsync(addLanguage, c =>
{
GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c);
AddLanguage(c);
});
case RemoveLanguage removeLanguage:
return UpdateAsync(removeLanguage, c =>
{
GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c);
RemoveLanguage(c);
});
case UpdateLanguage updateLanguage:
return UpdateAsync(updateLanguage, c =>
{
GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c);
UpdateLanguage(c);
});
case AddPattern addPattern:
return UpdateAsync(addPattern, c =>
{
GuardAppPattern.CanAdd(Snapshot.Patterns, c);
AddPattern(c);
});
case DeletePattern deletePattern:
return UpdateAsync(deletePattern, c =>
{
GuardAppPattern.CanDelete(Snapshot.Patterns, c);
DeletePattern(c);
});
case UpdatePattern updatePattern:
return UpdateAsync(updatePattern, c =>
{
GuardAppPattern.CanUpdate(Snapshot.Patterns, c);
UpdatePattern(c);
});
case ChangePlan changePlan:
return UpdateReturnAsync(changePlan, async c =>
{
GuardApp.CanChangePlan(c, Snapshot.Plan, appPlansProvider);
if (c.FromCallback)
{
ChangePlan(c);
return null;
}
else
{
var result = await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.Id, Snapshot.Name, c.PlanId);
if (result is PlanChangedResult)
{
ChangePlan(c);
}
return result;
}
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateApp command)
{
var appId = new NamedId<Guid>(command.AppId, command.Name);
var events = new List<AppEvent>
{
CreateInitalEvent(command.Name),
CreateInitialOwner(command.Actor),
CreateInitialLanguage()
};
foreach (var pattern in initialPatterns)
{
events.Add(CreateInitialPattern(pattern.Key, pattern.Value));
}
foreach (var @event in events)
{
@event.Actor = command.Actor;
@event.AppId = appId;
RaiseEvent(@event);
}
}
public void UpdateClient(UpdateClient command)
{
if (!string.IsNullOrWhiteSpace(command.Name))
{
RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed()));
}
if (command.Permission.HasValue)
{
RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Permission = command.Permission.Value }));
}
}
public void UpdateLanguage(UpdateLanguage command)
{
RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated()));
}
public void AssignContributor(AssignContributor command)
{
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned()));
}
public void RemoveContributor(RemoveContributor command)
{
RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved()));
}
public void AttachClient(AttachClient command)
{
RaiseEvent(SimpleMapper.Map(command, new AppClientAttached()));
}
public void RevokeClient(RevokeClient command)
{
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked()));
}
public void AddLanguage(AddLanguage command)
{
RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded()));
}
public void RemoveLanguage(RemoveLanguage command)
{
RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved()));
}
public void ChangePlan(ChangePlan command)
{
RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged()));
}
public void AddPattern(AddPattern command)
{
RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded()));
}
public void DeletePattern(DeletePattern command)
{
RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted()));
}
public void UpdatePattern(UpdatePattern command)
{
RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated()));
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = new NamedId<Guid>(Snapshot.Id, Snapshot.Name);
}
RaiseEvent(Envelope.Create(@event));
}
private static AppCreated CreateInitalEvent(string name)
{
return new AppCreated { Name = name };
}
private static AppPatternAdded CreateInitialPattern(Guid id, AppPattern pattern)
{
return new AppPatternAdded { PatternId = id, Name = pattern.Name, Pattern = pattern.Pattern, Message = pattern.Message };
}
private static AppLanguageAdded CreateInitialLanguage()
{
return new AppLanguageAdded { Language = Language.EN };
}
private static AppContributorAssigned CreateInitialOwner(RefToken actor)
{
return new AppContributorAssigned { ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner };
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!clients.TryGetValue(id, out var client))
{
throw new DomainObjectNotFoundException(id, "Clients", typeof(AppDomainObject));
throw new DomainObjectNotFoundException(id, "Clients", typeof(IAppEntity));
}
return client;

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!contributors.ContainsKey(command.ContributorId))
{
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject));
throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(IAppEntity));
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppLanguages.cs

@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (!languages.TryGetConfig(language, out var languageConfig))
{
throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject));
throw new DomainObjectNotFoundException(language, "Languages", typeof(IAppEntity));
}
return languageConfig;

2
src/Squidex.Domain.Apps.Entities/Apps/Services/Implementations/NoopAppPlanBillingManager.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services.Implementations
public Task<IChangePlanResult> ChangePlanAsync(string userId, Guid appId, string appName, string planId)
{
return Task.FromResult<IChangePlanResult>(PlanChangedResult.Instance);
return Task.FromResult<IChangePlanResult>(new PlanChangedResult());
}
public Task<string> GetPortalLinkAsync(string userId)

5
src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangeAsyncResult.cs

@ -9,10 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services
{
public sealed class PlanChangeAsyncResult : IChangePlanResult
{
public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult();
private PlanChangeAsyncResult()
{
}
}
}

5
src/Squidex.Domain.Apps.Entities/Apps/Services/PlanChangedResult.cs

@ -9,10 +9,5 @@ namespace Squidex.Domain.Apps.Entities.Apps.Services
{
public sealed class PlanChangedResult : IChangePlanResult
{
public static readonly PlanChangedResult Instance = new PlanChangedResult();
private PlanChangedResult()
{
}
}
}

130
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -8,108 +8,80 @@
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetCommandMiddleware : ICommandMiddleware
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, AssetGrain>
{
private readonly IAggregateHandler handler;
private readonly IAssetStore assetStore;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetCommandMiddleware(
IAggregateHandler handler,
IStateFactory stateFactory,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(stateFactory)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
this.handler = handler;
this.assetStore = assetStore;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
protected async Task On(CreateAsset command, CommandContext context)
public async override Task HandleAsync(CommandContext context, Func<Task> next)
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
try
switch (context.Command)
{
var asset = await handler.CreateSyncedAsync<AssetDomainObject>(context, async a =>
{
GuardAsset.CanCreate(command);
a.Create(command);
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead());
context.Complete(EntityCreatedResult.Create(command.AssetId, a.Version));
});
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.Snapshot.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
}
protected async Task On(UpdateAsset command, CommandContext context)
{
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
try
{
var asset = await handler.UpdateSyncedAsync<AssetDomainObject>(context, async a =>
{
GuardAsset.CanUpdate(command);
a.Update(command);
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead());
context.Complete(new AssetSavedResult(a.Version, a.Snapshot.FileVersion));
});
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), command.AssetId.ToString(), asset.Snapshot.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
}
protected Task On(RenameAsset command, CommandContext context)
{
return handler.UpdateSyncedAsync<AssetDomainObject>(context, a =>
{
GuardAsset.CanRename(command, a.Snapshot.FileName);
a.Rename(command);
});
}
protected Task On(DeleteAsset command, CommandContext context)
{
return handler.UpdateSyncedAsync<AssetDomainObject>(context, a =>
{
GuardAsset.CanDelete(command);
a.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
case CreateAsset createAsset:
{
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), createAsset.File.OpenRead());
try
{
var result = await ExecuteCommandAsync(createAsset) as AssetSavedResult;
context.Complete(EntityCreatedResult.Create(createAsset.AssetId, result.Version));
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
break;
}
case UpdateAsset updateAsset:
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), updateAsset.File.OpenRead());
try
{
var result = await ExecuteCommandAsync(updateAsset) as AssetSavedResult;
context.Complete(result);
await assetStore.CopyTemporaryAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
}
finally
{
await assetStore.DeleteTemporaryAsync(context.ContextId.ToString());
}
break;
}
default:
await base.HandleAsync(context, next);
break;
}
}
}

89
src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs → src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -5,22 +5,71 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetDomainObject : SquidexDomainObjectBase<AssetState>
public class AssetGrain : DomainObjectGrain<AssetState>
{
public AssetDomainObject Create(CreateAsset command)
public AssetGrain(IStore<Guid> store)
: base(store)
{
VerifyNotCreated();
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case CreateAsset createRule:
return CreateReturnAsync(createRule, c =>
{
GuardAsset.CanCreate(c);
Create(c);
return new AssetSavedResult(NewVersion, Snapshot.FileVersion);
});
case UpdateAsset updateRule:
return UpdateReturnAsync(updateRule, c =>
{
GuardAsset.CanUpdate(c);
Update(c);
return new AssetSavedResult(NewVersion, Snapshot.FileVersion);
});
case RenameAsset renameAsset:
return UpdateAsync(renameAsset, c =>
{
GuardAsset.CanRename(c, Snapshot.FileName);
Rename(c);
});
case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, c =>
{
GuardAsset.CanDelete(c);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateAsset command)
{
var @event = SimpleMapper.Map(command, new AssetCreated
{
FileName = command.File.FileName,
@ -33,13 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
});
RaiseEvent(@event);
return this;
}
public AssetDomainObject Update(UpdateAsset command)
public void Update(UpdateAsset command)
{
VerifyCreatedAndNotDeleted();
VerifyNotDeleted();
var @event = SimpleMapper.Map(command, new AssetUpdated
{
@ -52,26 +99,20 @@ namespace Squidex.Domain.Apps.Entities.Assets
});
RaiseEvent(@event);
return this;
}
public AssetDomainObject Delete(DeleteAsset command)
public void Delete(DeleteAsset command)
{
VerifyCreatedAndNotDeleted();
VerifyNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }));
return this;
}
public AssetDomainObject Rename(RenameAsset command)
public void Rename(RenameAsset command)
{
VerifyCreatedAndNotDeleted();
VerifyNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new AssetRenamed()));
return this;
}
private void RaiseEvent(AppEvent @event)
@ -84,19 +125,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotCreated()
{
if (!string.IsNullOrWhiteSpace(Snapshot.FileName))
{
throw new DomainException("Asset has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted || string.IsNullOrWhiteSpace(Snapshot.FileName))
if (Snapshot.IsDeleted)
{
throw new DomainException("Asset has already been deleted or not created yet.");
throw new DomainException("Asset has already been deleted");
}
}

158
src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -1,158 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentCommandMiddleware : ICommandMiddleware
{
private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
public ContentCommandMiddleware(
IAggregateHandler handler,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.handler = handler;
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
protected async Task On(CreateContent command, CommandContext context)
{
await handler.CreateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanCreate(command);
var operationContext = await CreateContext(command, content, () => "Failed to create content.");
if (command.Publish)
{
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published");
}
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create");
await operationContext.EnrichAsync();
await operationContext.ValidateAsync(false);
content.Create(command);
context.Complete(EntityCreatedResult.Create(command.Data, content.Version));
});
}
protected async Task On(UpdateContent command, CommandContext context)
{
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanUpdate(command);
var operationContext = await CreateContext(command, content, () => "Failed to update content.");
await operationContext.ValidateAsync(true);
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update");
content.Update(command);
context.Complete(new ContentDataChangedResult(content.Snapshot.Data, content.Version));
});
}
protected async Task On(PatchContent command, CommandContext context)
{
await handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanPatch(command);
var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
await operationContext.ValidateAsync(true);
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch");
content.Patch(command);
context.Complete(new ContentDataChangedResult(content.Snapshot.Data, content.Version));
});
}
protected Task On(ChangeContentStatus command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanChangeContentStatus(content.Snapshot.Status, command);
if (!command.DueTime.HasValue)
{
var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status);
}
content.ChangeStatus(command);
});
}
protected Task On(DeleteContent command, CommandContext context)
{
return handler.UpdateAsync<ContentDomainObject>(context, async content =>
{
GuardContent.CanDelete(command);
var operationContext = await CreateContext(command, content, () => "Failed to delete content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete");
content.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await this.DispatchActionAsync(context.Command, context);
await next();
}
private async Task<ContentOperationContext> CreateContext(ContentCommand command, ContentDomainObject content, Func<string> message)
{
var operationContext =
await ContentOperationContext.CreateAsync(
contentRepository,
content,
command,
appProvider,
assetRepository,
scriptEngine,
message);
return operationContext;
}
}
}

126
src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -1,126 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentDomainObject : SquidexDomainObjectBase<ContentState>
{
public ContentDomainObject Create(CreateContent command)
{
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
return this;
}
public ContentDomainObject Delete(DeleteContent command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted()));
return this;
}
public ContentDomainObject ChangeStatus(ChangeContentStatus command)
{
VerifyCreatedAndNotDeleted();
if (command.DueTime.HasValue)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
}
else
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
}
return this;
}
public ContentDomainObject Update(UpdateContent command)
{
VerifyCreatedAndNotDeleted();
if (!command.Data.Equals(Snapshot.Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated()));
}
return this;
}
public ContentDomainObject Patch(PatchContent command)
{
VerifyCreatedAndNotDeleted();
var newData = command.Data.MergeInto(Snapshot.Data);
if (!newData.Equals(Snapshot.Data))
{
var @event = SimpleMapper.Map(command, new ContentUpdated());
@event.Data = newData;
RaiseEvent(@event);
}
return this;
}
private void RaiseEvent(SchemaEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
if (@event.SchemaId == null)
{
@event.SchemaId = Snapshot.SchemaId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotCreated()
{
if (Snapshot.Data != null)
{
throw new DomainException("Content has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (Snapshot.IsDeleted || Snapshot.Data == null)
{
throw new DomainException("Content has already been deleted or not created yet.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
}
}

232
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -0,0 +1,232 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentGrain : DomainObjectGrain<ContentState>
{
private readonly IAppProvider appProvider;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
public ContentGrain(
IStore<Guid> store,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentRepository contentRepository)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
switch (command)
{
case CreateContent createContent:
return CreateReturnAsync(createContent, async c =>
{
GuardContent.CanCreate(c);
var operationContext = await CreateContext(c, () => "Failed to create content.");
if (c.Publish)
{
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published");
}
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create");
await operationContext.EnrichAsync();
await operationContext.ValidateAsync();
Create(c);
return EntityCreatedResult.Create(c.Data, NewVersion);
});
case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, async c =>
{
GuardContent.CanUpdate(c);
var operationContext = await CreateContext(c, () => "Failed to update content.");
await operationContext.ValidateAsync();
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update");
Update(c);
return new ContentDataChangedResult(Snapshot.Data, NewVersion);
});
case PatchContent patchContent:
return UpdateReturnAsync(patchContent, async c =>
{
GuardContent.CanPatch(c);
var operationContext = await CreateContext(c, () => "Failed to patch content.");
await operationContext.ValidatePartialAsync();
await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch");
Patch(c);
return new ContentDataChangedResult(Snapshot.Data, NewVersion);
});
case ChangeContentStatus patchContent:
return UpdateAsync(patchContent, async c =>
{
GuardContent.CanChangeContentStatus(Snapshot.Status, c);
if (!c.DueTime.HasValue)
{
var operationContext = await CreateContext(c, () => "Failed to patch content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptChange, c.Status);
}
ChangeStatus(c);
});
case DeleteContent deleteContent:
return UpdateAsync(deleteContent, async c =>
{
GuardContent.CanDelete(c);
var operationContext = await CreateContext(c, () => "Failed to delete content.");
await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete");
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateContent command)
{
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
}
public void Update(UpdateContent command)
{
if (!command.Data.Equals(Snapshot.Data))
{
RaiseEvent(SimpleMapper.Map(command, new ContentUpdated()));
}
}
public void ChangeStatus(ChangeContentStatus command)
{
if (command.DueTime.HasValue)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
}
else
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
}
}
public void Patch(PatchContent command)
{
var newData = command.Data.MergeInto(Snapshot.Data);
if (!newData.Equals(Snapshot.Data))
{
var @event = SimpleMapper.Map(command, new ContentUpdated());
@event.Data = newData;
RaiseEvent(@event);
}
}
public void Delete(DeleteContent command)
{
RaiseEvent(SimpleMapper.Map(command, new ContentDeleted()));
}
private void RaiseEvent(SchemaEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
if (@event.SchemaId == null)
{
@event.SchemaId = Snapshot.SchemaId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted)
{
throw new DomainException("Content has already been deleted.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
private async Task<ContentOperationContext> CreateContext(ContentCommand command, Func<string> message)
{
var operationContext =
await ContentOperationContext.CreateAsync(command, Snapshot,
contentRepository,
appProvider,
assetRepository,
scriptEngine,
message);
return operationContext;
}
}
}

89
src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.EnrichContent;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
@ -17,16 +18,15 @@ using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentOperationContext
{
private ContentDomainObject content;
private ContentCommand command;
private IContentRepository contentRepository;
private IContentEntity content;
private IAssetRepository assetRepository;
private IScriptEngine scriptEngine;
private ISchemaEntity schemaEntity;
@ -35,16 +35,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
private Func<string> message;
public static async Task<ContentOperationContext> CreateAsync(
IContentRepository contentRepository,
ContentDomainObject content,
ContentCommand command,
IContentEntity content,
IContentRepository contentRepository,
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
Func<string> message)
{
var a = content.Snapshot.AppId;
var s = content.Snapshot.SchemaId;
var a = content.AppId;
var s = content.SchemaId;
if (command is CreateContent createContent)
{
@ -80,54 +80,35 @@ namespace Squidex.Domain.Apps.Entities.Contents
return TaskHelper.Done;
}
public async Task ValidateAsync(bool partial)
public Task ValidateAsync()
{
if (command is ContentDataCommand dataCommand)
{
var errors = new List<ValidationError>();
var ctx =
new ValidationContext(
(contentIds, schemaId) =>
{
return QueryContentsAsync(schemaId, contentIds);
},
assetIds =>
{
return QueryAssetsAsync(assetIds);
});
if (partial)
{
await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors);
}
else
{
await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors);
}
var ctx = CreateValidationContext();
if (errors.Count > 0)
{
throw new ValidationException(message(), errors.ToArray());
}
return dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
}
}
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
{
return await assetRepository.QueryAsync(appId, new HashSet<Guid>(assetIds));
return TaskHelper.Done;
}
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid schemaId, IEnumerable<Guid> contentIds)
public Task ValidatePartialAsync()
{
return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList());
if (command is ContentDataCommand dataCommand)
{
var ctx = CreateValidationContext();
return dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), message);
}
return TaskHelper.Done;
}
public Task ExecuteScriptAndTransformAsync(Func<ISchemaEntity, string> script, object operation)
{
if (command is ContentDataCommand dataCommand)
{
var ctx = new ScriptContext { ContentId = content.Snapshot.Id, OldData = content.Snapshot.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data };
var ctx = CreateScriptContext(operation, dataCommand.Data);
dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity));
}
@ -137,11 +118,39 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Task ExecuteScriptAsync(Func<ISchemaEntity, string> script, object operation)
{
var ctx = new ScriptContext { ContentId = content.Snapshot.Id, OldData = content.Snapshot.Data, User = command.User, Operation = operation.ToString() };
var ctx = CreateScriptContext(operation, content.Data);
scriptEngine.Execute(ctx, script(schemaEntity));
return TaskHelper.Done;
}
private ScriptContext CreateScriptContext(object operation, NamedContentData data = null)
{
return new ScriptContext { ContentId = command.ContentId, OldData = content.Data, Data = data, User = command.User, Operation = operation.ToString() };
}
private ValidationContext CreateValidationContext()
{
return new ValidationContext(
(contentIds, schemaId) =>
{
return QueryContentsAsync(schemaId, contentIds);
},
assetIds =>
{
return QueryAssetsAsync(assetIds);
});
}
private async Task<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
{
return await assetRepository.QueryAsync(appId, new HashSet<Guid>(assetIds));
}
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid schemaId, IEnumerable<Guid> contentIds)
{
return await contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList());
}
}
}

89
src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs

@ -1,89 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Guards;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleCommandMiddleware : ICommandMiddleware
{
private readonly IAggregateHandler handler;
private readonly IAppProvider appProvider;
public RuleCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
this.handler = handler;
this.appProvider = appProvider;
}
protected Task On(CreateRule command, CommandContext context)
{
return handler.CreateSyncedAsync<RuleDomainObject>(context, async r =>
{
await GuardRule.CanCreate(command, appProvider);
r.Create(command);
});
}
protected Task On(UpdateRule command, CommandContext context)
{
return handler.UpdateSyncedAsync<RuleDomainObject>(context, async r =>
{
await GuardRule.CanUpdate(command, r.Snapshot.AppId.Id, appProvider);
r.Update(command);
});
}
protected Task On(EnableRule command, CommandContext context)
{
return handler.UpdateSyncedAsync<RuleDomainObject>(context, r =>
{
GuardRule.CanEnable(command, r.Snapshot.RuleDef);
r.Enable(command);
});
}
protected Task On(DisableRule command, CommandContext context)
{
return handler.UpdateSyncedAsync<RuleDomainObject>(context, r =>
{
GuardRule.CanDisable(command, r.Snapshot.RuleDef);
r.Disable(command);
});
}
protected Task On(DeleteRule command, CommandContext context)
{
return handler.UpdateSyncedAsync<RuleDomainObject>(context, r =>
{
GuardRule.CanDelete(command);
r.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await this.DispatchActionAsync(context.Command, context);
await next();
}
}
}

86
src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs

@ -1,86 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Rules
{
public sealed class RuleDomainObject : SquidexDomainObjectBase<RuleState>
{
public void Create(CreateRule command)
{
VerifyNotCreated();
RaiseEvent(SimpleMapper.Map(command, new RuleCreated()));
}
public void Update(UpdateRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleUpdated()));
}
public void Enable(EnableRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleEnabled()));
}
public void Disable(DisableRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleDisabled()));
}
public void Delete(DeleteRule command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new RuleDeleted()));
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotCreated()
{
if (Snapshot.RuleDef != null)
{
throw new DomainException("Webhook has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (Snapshot.IsDeleted || Snapshot.RuleDef == null)
{
throw new DomainException("Webhook has already been deleted or not created yet.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
}
}

129
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -0,0 +1,129 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Guards;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleGrain : DomainObjectGrain<RuleState>
{
private readonly IAppProvider appProvider;
public RuleGrain(IStore<Guid> store, IAppProvider appProvider)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
switch (command)
{
case CreateRule createRule:
return CreateAsync(createRule, c =>
{
GuardRule.CanCreate(c, appProvider);
Create(c);
});
case UpdateRule updateRule:
return UpdateAsync(updateRule, c =>
{
GuardRule.CanUpdate(c, Snapshot.AppId.Id, appProvider);
Update(c);
});
case EnableRule enableRule:
return UpdateAsync(enableRule, c =>
{
GuardRule.CanEnable(c, Snapshot.RuleDef);
Enable(c);
});
case DisableRule disableRule:
return UpdateAsync(disableRule, c =>
{
GuardRule.CanDisable(c, Snapshot.RuleDef);
Disable(c);
});
case DeleteRule deleteRule:
return UpdateAsync(deleteRule, c =>
{
GuardRule.CanDelete(deleteRule);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateRule command)
{
RaiseEvent(SimpleMapper.Map(command, new RuleCreated()));
}
public void Update(UpdateRule command)
{
RaiseEvent(SimpleMapper.Map(command, new RuleUpdated()));
}
public void Enable(EnableRule command)
{
RaiseEvent(SimpleMapper.Map(command, new RuleEnabled()));
}
public void Disable(DisableRule command)
{
RaiseEvent(SimpleMapper.Map(command, new RuleDisabled()));
}
public void Delete(DeleteRule command)
{
RaiseEvent(SimpleMapper.Map(command, new RuleDeleted()));
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted)
{
throw new DomainException("Webhook has already been deleted.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event));
}
}
}

194
src/Squidex.Domain.Apps.Entities/Schemas/SchemaCommandMiddleware.cs

@ -1,194 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Guards;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Dispatching;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaCommandMiddleware : ICommandMiddleware
{
private readonly IAppProvider appProvider;
private readonly IAggregateHandler handler;
public SchemaCommandMiddleware(IAggregateHandler handler, IAppProvider appProvider)
{
Guard.NotNull(handler, nameof(handler));
Guard.NotNull(appProvider, nameof(appProvider));
this.handler = handler;
this.appProvider = appProvider;
}
protected Task On(CreateSchema command, CommandContext context)
{
return handler.CreateSyncedAsync<SchemaDomainObject>(context, async s =>
{
await GuardSchema.CanCreate(command, appProvider);
s.Create(command);
context.Complete(EntityCreatedResult.Create(command.SchemaId, s.Version));
});
}
protected Task On(AddField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanAdd(s.Snapshot.SchemaDef, command);
s.Add(command);
context.Complete(EntityCreatedResult.Create(s.Snapshot.SchemaDef.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version));
});
}
protected Task On(DeleteField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanDelete(s.Snapshot.SchemaDef, command);
s.DeleteField(command);
});
}
protected Task On(LockField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanLock(s.Snapshot.SchemaDef, command);
s.LockField(command);
});
}
protected Task On(HideField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanHide(s.Snapshot.SchemaDef, command);
s.HideField(command);
});
}
protected Task On(ShowField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanShow(s.Snapshot.SchemaDef, command);
s.ShowField(command);
});
}
protected Task On(DisableField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanDisable(s.Snapshot.SchemaDef, command);
s.DisableField(command);
});
}
protected Task On(EnableField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanEnable(s.Snapshot.SchemaDef, command);
s.EnableField(command);
});
}
protected Task On(UpdateField command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchemaField.CanUpdate(s.Snapshot.SchemaDef, command);
s.UpdateField(command);
});
}
protected Task On(ReorderFields command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanReorder(s.Snapshot.SchemaDef, command);
s.Reorder(command);
});
}
protected Task On(UpdateSchema command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanUpdate(s.Snapshot.SchemaDef, command);
s.Update(command);
});
}
protected Task On(PublishSchema command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanPublish(s.Snapshot.SchemaDef, command);
s.Publish(command);
});
}
protected Task On(UnpublishSchema command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanUnpublish(s.Snapshot.SchemaDef, command);
s.Unpublish(command);
});
}
protected Task On(ConfigureScripts command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanConfigureScripts(s.Snapshot.SchemaDef, command);
s.ConfigureScripts(command);
});
}
protected Task On(DeleteSchema command, CommandContext context)
{
return handler.UpdateSyncedAsync<SchemaDomainObject>(context, s =>
{
GuardSchema.CanDelete(s.Snapshot.SchemaDef, command);
s.Delete(command);
});
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await this.DispatchActionAsync(context.Command, context);
await next();
}
}
}

229
src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs

@ -1,229 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public sealed class SchemaDomainObject : SquidexDomainObjectBase<SchemaState>
{
private readonly FieldRegistry registry;
public SchemaDomainObject(FieldRegistry registry)
{
Guard.NotNull(registry, nameof(registry));
this.registry = registry;
}
public SchemaDomainObject Create(CreateSchema command)
{
VerifyNotCreated();
var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId<Guid>(command.SchemaId, command.Name) });
if (command.Fields != null)
{
@event.Fields = new List<SchemaCreatedField>();
foreach (var commandField in command.Fields)
{
var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField());
@event.Fields.Add(eventField);
}
}
RaiseEvent(@event);
return this;
}
public SchemaDomainObject Add(AddField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId<long>(Snapshot.TotalFields + 1, command.Name) }));
return this;
}
public SchemaDomainObject UpdateField(UpdateField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated()));
return this;
}
public SchemaDomainObject LockField(LockField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldLocked());
return this;
}
public SchemaDomainObject HideField(HideField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldHidden());
return this;
}
public SchemaDomainObject ShowField(ShowField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldShown());
return this;
}
public SchemaDomainObject DisableField(DisableField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldDisabled());
return this;
}
public SchemaDomainObject EnableField(EnableField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldEnabled());
return this;
}
public SchemaDomainObject DeleteField(DeleteField command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(command, new FieldDeleted());
return this;
}
public SchemaDomainObject Reorder(ReorderFields command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered()));
return this;
}
public SchemaDomainObject Publish(PublishSchema command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new SchemaPublished()));
return this;
}
public SchemaDomainObject Unpublish(UnpublishSchema command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished()));
return this;
}
public SchemaDomainObject ConfigureScripts(ConfigureScripts command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured()));
return this;
}
public SchemaDomainObject Delete(DeleteSchema command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted()));
return this;
}
public SchemaDomainObject Update(UpdateSchema command)
{
VerifyCreatedAndNotDeleted();
RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated()));
return this;
}
private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event)
{
SimpleMapper.Map(fieldCommand, @event);
if (Snapshot.SchemaDef.FieldsById.TryGetValue(fieldCommand.FieldId, out var field))
{
@event.FieldId = new NamedId<long>(field.Id, field.Name);
}
RaiseEvent(@event);
}
private void RaiseEvent(SchemaEvent @event)
{
if (@event.SchemaId == null)
{
@event.SchemaId = new NamedId<Guid>(Snapshot.Id, Snapshot.Name);
}
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotCreated()
{
if (Snapshot.SchemaDef != null)
{
throw new DomainException("Schema has already been created.");
}
}
private void VerifyCreatedAndNotDeleted()
{
if (Snapshot.IsDeleted || Snapshot.SchemaDef == null)
{
throw new DomainException("Schema has already been deleted or not created yet.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event, registry));
}
}
}

304
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -0,0 +1,304 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.Guards;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaGrain : DomainObjectGrain<SchemaState>
{
private readonly IAppProvider appProvider;
private readonly FieldRegistry registry;
public SchemaGrain(IStore<Guid> store, IAppProvider appProvider, FieldRegistry registry)
: base(store)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(registry, nameof(registry));
this.appProvider = appProvider;
this.registry = registry;
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
switch (command)
{
case CreateSchema createSchema:
return CreateAsync(createSchema, async c =>
{
await GuardSchema.CanCreate(c, appProvider);
Create(c);
});
case AddField addField:
return UpdateReturnAsync(addField, c =>
{
GuardSchemaField.CanAdd(Snapshot.SchemaDef, c);
Add(c);
return EntityCreatedResult.Create(Snapshot.SchemaDef.FieldsById.Values.First(x => x.Name == addField.Name).Id, NewVersion);
});
case DeleteField deleteField:
return UpdateAsync(deleteField, c =>
{
GuardSchemaField.CanDelete(Snapshot.SchemaDef, deleteField);
DeleteField(c);
});
case LockField lockField:
return UpdateAsync(lockField, c =>
{
GuardSchemaField.CanLock(Snapshot.SchemaDef, lockField);
LockField(c);
});
case HideField hideField:
return UpdateAsync(hideField, c =>
{
GuardSchemaField.CanHide(Snapshot.SchemaDef, c);
HideField(c);
});
case ShowField showField:
return UpdateAsync(showField, c =>
{
GuardSchemaField.CanShow(Snapshot.SchemaDef, c);
ShowField(c);
});
case DisableField disableField:
return UpdateAsync(disableField, c =>
{
GuardSchemaField.CanDisable(Snapshot.SchemaDef, c);
DisableField(c);
});
case EnableField enableField:
return UpdateAsync(enableField, c =>
{
GuardSchemaField.CanEnable(Snapshot.SchemaDef, c);
EnableField(c);
});
case UpdateField updateField:
return UpdateAsync(updateField, c =>
{
GuardSchemaField.CanUpdate(Snapshot.SchemaDef, c);
UpdateField(c);
});
case ReorderFields reorderFields:
return UpdateAsync(reorderFields, c =>
{
GuardSchema.CanReorder(Snapshot.SchemaDef, c);
Reorder(c);
});
case UpdateSchema updateSchema:
return UpdateAsync(updateSchema, c =>
{
GuardSchema.CanUpdate(Snapshot.SchemaDef, c);
Update(c);
});
case PublishSchema publishSchema:
return UpdateAsync(publishSchema, c =>
{
GuardSchema.CanPublish(Snapshot.SchemaDef, c);
Publish(c);
});
case UnpublishSchema unpublishSchema:
return UpdateAsync(unpublishSchema, c =>
{
GuardSchema.CanUnpublish(Snapshot.SchemaDef, c);
Unpublish(c);
});
case ConfigureScripts configureScripts:
return UpdateAsync(configureScripts, c =>
{
GuardSchema.CanConfigureScripts(Snapshot.SchemaDef, c);
ConfigureScripts(c);
});
case DeleteSchema deleteSchema:
return UpdateAsync(deleteSchema, c =>
{
GuardSchema.CanDelete(Snapshot.SchemaDef, c);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateSchema command)
{
var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId<Guid>(command.SchemaId, command.Name) });
if (command.Fields != null)
{
@event.Fields = new List<SchemaCreatedField>();
foreach (var commandField in command.Fields)
{
var eventField = SimpleMapper.Map(commandField, new SchemaCreatedField());
@event.Fields.Add(eventField);
}
}
RaiseEvent(@event);
}
public void Add(AddField command)
{
RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId<long>(Snapshot.TotalFields + 1, command.Name) }));
}
public void UpdateField(UpdateField command)
{
RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated()));
}
public void LockField(LockField command)
{
RaiseEvent(command, new FieldLocked());
}
public void HideField(HideField command)
{
RaiseEvent(command, new FieldHidden());
}
public void ShowField(ShowField command)
{
RaiseEvent(command, new FieldShown());
}
public void DisableField(DisableField command)
{
RaiseEvent(command, new FieldDisabled());
}
public void EnableField(EnableField command)
{
RaiseEvent(command, new FieldEnabled());
}
public void DeleteField(DeleteField command)
{
RaiseEvent(command, new FieldDeleted());
}
public void Reorder(ReorderFields command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered()));
}
public void Publish(PublishSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaPublished()));
}
public void Unpublish(UnpublishSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished()));
}
public void ConfigureScripts(ConfigureScripts command)
{
RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured()));
}
public void Delete(DeleteSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaDeleted()));
}
public void Update(UpdateSchema command)
{
RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated()));
}
private void RaiseEvent(FieldCommand fieldCommand, FieldEvent @event)
{
SimpleMapper.Map(fieldCommand, @event);
if (Snapshot.SchemaDef.FieldsById.TryGetValue(fieldCommand.FieldId, out var field))
{
@event.FieldId = new NamedId<long>(field.Id, field.Name);
}
RaiseEvent(@event);
}
private void RaiseEvent(SchemaEvent @event)
{
if (@event.SchemaId == null)
{
@event.SchemaId = new NamedId<Guid>(Snapshot.Id, Snapshot.Name);
}
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted)
{
throw new DomainException("Schema has already been deleted.");
}
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
ApplySnapshot(Snapshot.Apply(@event, registry));
}
}
}

26
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectBase.cs

@ -1,26 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities
{
public abstract class SquidexDomainObjectBase<T> : DomainObjectBase<T> where T : IDomainState, new()
{
public override void RaiseEvent(Envelope<IEvent> @event)
{
if (@event.Payload is AppEvent appEvent)
{
@event.SetAppId(appEvent.AppId.Id);
}
base.RaiseEvent(@event);
}
}
}

19
src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConverter.cs

@ -81,8 +81,6 @@ namespace Squidex.Infrastructure.MongoDb
return BsonNull.Value;
case JTokenType.Undefined:
return BsonUndefined.Value;
case JTokenType.Date:
return BsonValue.Create(((JValue)source).ToString("yyyy-MM-ddTHH:mm:ssK"));
case JTokenType.Bytes:
return BsonValue.Create(((JValue)source).Value);
case JTokenType.Guid:
@ -91,6 +89,23 @@ namespace Squidex.Infrastructure.MongoDb
return BsonValue.Create(((JValue)source).ToString());
case JTokenType.TimeSpan:
return BsonValue.Create(((JValue)source).ToString());
case JTokenType.Date:
{
var value = ((JValue)source).Value;
if (value is DateTime dateTime)
{
return dateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
}
else if (value is DateTimeOffset dateTimeOffset)
{
return dateTimeOffset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssK");
}
else
{
return value.ToString();
}
}
}
throw new NotSupportedException($"Cannot convert {source.GetType()} to Bson.");

13
src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -68,13 +68,16 @@ namespace Squidex.Infrastructure.MongoDb
{
return new Lazy<IMongoCollection<TEntity>>(() =>
{
var databaseCollection = mongoDatabase.GetCollection<TEntity>(
CollectionName(),
CollectionSettings() ?? new MongoCollectionSettings());
return Task.Run(async () =>
{
var databaseCollection = mongoDatabase.GetCollection<TEntity>(
CollectionName(),
CollectionSettings() ?? new MongoCollectionSettings());
SetupCollectionAsync(databaseCollection).Wait();
await SetupCollectionAsync(databaseCollection).ConfigureAwait(false);
return databaseCollection;
return databaseCollection;
}).Result;
});
}

149
src/Squidex.Infrastructure/Commands/AggregateHandler.cs

@ -1,149 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Commands
{
public sealed class AggregateHandler : IAggregateHandler
{
private readonly AsyncLockPool lockPool = new AsyncLockPool(10000);
private readonly IStateFactory stateFactory;
private readonly IServiceProvider serviceProvider;
public AggregateHandler(IStateFactory stateFactory, IServiceProvider serviceProvider)
{
Guard.NotNull(stateFactory, nameof(stateFactory));
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.stateFactory = stateFactory;
this.serviceProvider = serviceProvider;
}
public Task<T> CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IDomainObject
{
Guard.NotNull(creator, nameof(creator));
return InvokeAsync(context, creator, false);
}
public Task<T> UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IDomainObject
{
Guard.NotNull(updater, nameof(updater));
return InvokeAsync(context, updater, true);
}
public Task<T> CreateSyncedAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IDomainObject
{
Guard.NotNull(creator, nameof(creator));
return InvokeSyncedAsync(context, creator, false);
}
public Task<T> UpdateSyncedAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IDomainObject
{
Guard.NotNull(updater, nameof(updater));
return InvokeSyncedAsync(context, updater, true);
}
private async Task<T> InvokeAsync<T>(CommandContext context, Func<T, Task> handler, bool isUpdate) where T : class, IDomainObject
{
Guard.NotNull(context, nameof(context));
var domainCommand = GetCommand(context);
var domainObjectId = domainCommand.AggregateId;
var domainObject = await stateFactory.CreateAsync<T>(domainObjectId);
if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version)
{
throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion);
}
await handler(domainObject);
await domainObject.WriteAsync();
if (!context.IsCompleted)
{
if (isUpdate)
{
context.Complete(new EntitySavedResult(domainObject.Version));
}
else
{
context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version));
}
}
return domainObject;
}
private async Task<T> InvokeSyncedAsync<T>(CommandContext context, Func<T, Task> handler, bool isUpdate) where T : class, IDomainObject
{
Guard.NotNull(context, nameof(context));
var domainCommand = GetCommand(context);
var domainObjectId = domainCommand.AggregateId;
using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId)))
{
var domainObject = await stateFactory.GetSingleAsync<T>(domainObjectId);
if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version)
{
throw new DomainObjectVersionException(domainObjectId.ToString(), typeof(T), domainObject.Version, domainCommand.ExpectedVersion);
}
await handler(domainObject);
try
{
await domainObject.WriteAsync();
stateFactory.Synchronize<T, Guid>(domainObjectId);
}
catch
{
stateFactory.Remove<T, Guid>(domainObjectId);
throw;
}
if (!context.IsCompleted)
{
if (isUpdate)
{
context.Complete(new EntitySavedResult(domainObject.Version));
}
else
{
context.Complete(EntityCreatedResult.Create(domainObjectId, domainObject.Version));
}
}
return domainObject;
}
}
private static IAggregateCommand GetCommand(CommandContext context)
{
if (!(context.Command is IAggregateCommand command))
{
throw new ArgumentException("Context must have an aggregate command.", nameof(context));
}
Guard.NotEmpty(command.AggregateId, "context.Command.AggregateId");
return command;
}
}
}

21
src/Squidex.Infrastructure/Commands/CommandExtensions.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
@ -13,26 +12,6 @@ namespace Squidex.Infrastructure.Commands
{
public static class CommandExtensions
{
public static Task<T> CreateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{
return handler.CreateAsync(context, creator.ToAsync());
}
public static Task<T> UpdateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{
return handler.UpdateAsync(context, updater.ToAsync());
}
public static Task<T> CreateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{
return handler.CreateSyncedAsync(context, creator.ToAsync());
}
public static Task<T> UpdateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{
return handler.UpdateSyncedAsync(context, updater.ToAsync());
}
public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context)
{
return commandMiddleware.HandleAsync(context, () => TaskHelper.Done);

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

@ -1,106 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public abstract class DomainObjectBase<T> : IDomainObject where T : IDomainState, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private Guid id;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T> persistence;
public long Version
{
get { return snapshot.Version; }
}
public T Snapshot
{
get { return snapshot; }
}
public Task ActivateAsync(Guid key, IStore<Guid> store)
{
id = key;
persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(key, ApplySnapshot, ApplyEvent);
return persistence.ReadAsync();
}
public void RaiseEvent(IEvent @event)
{
RaiseEvent(Envelope.Create(@event));
}
public virtual void RaiseEvent(Envelope<IEvent> @event)
{
Guard.NotNull(@event, nameof(@event));
@event.SetAggregateId(id);
ApplyEvent(@event);
snapshot.Version++;
uncomittedEvents.Add(@event);
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
{
return uncomittedEvents;
}
public void ClearUncommittedEvents()
{
uncomittedEvents.Clear();
}
public virtual void ApplySnapshot(T newSnapshot)
{
snapshot = newSnapshot;
}
public virtual void ApplyEvent(Envelope<IEvent> @event)
{
}
public Task WriteSnapshotAsync()
{
snapshot.Version = persistence.Version;
return persistence.WriteSnapshotAsync(snapshot);
}
public async Task WriteAsync()
{
var events = uncomittedEvents.ToArray();
if (events.Length > 0)
{
try
{
snapshot.Version = persistence.Version + events.Length;
await persistence.WriteEventsAsync(events);
await persistence.WriteSnapshotAsync(snapshot);
}
finally
{
uncomittedEvents.Clear();
}
}
}
}
}

204
src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -0,0 +1,204 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Commands
{
public abstract class DomainObjectGrain<T> : IDomainObjectGrain where T : IDomainState, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private readonly IStore<Guid> store;
private Guid id;
private T snapshot = new T { Version = EtagVersion.Empty };
private IPersistence<T> persistence;
public Guid Id
{
get { return id; }
}
public long Version
{
get { return snapshot.Version; }
}
public long NewVersion
{
get { return snapshot.Version + uncomittedEvents.Count; }
}
public T Snapshot
{
get { return snapshot; }
}
protected DomainObjectGrain(IStore<Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public Task ActivateAsync(Guid key)
{
id = key;
persistence = store.WithSnapshotsAndEventSourcing<T, Guid>(GetType(), key, ApplySnapshot, ApplyEvent);
return persistence.ReadAsync();
}
public void RaiseEvent(IEvent @event)
{
RaiseEvent(Envelope.Create(@event));
}
public virtual void RaiseEvent(Envelope<IEvent> @event)
{
Guard.NotNull(@event, nameof(@event));
@event.SetAggregateId(Id);
ApplyEvent(@event);
uncomittedEvents.Add(@event);
}
public IReadOnlyList<Envelope<IEvent>> GetUncomittedEvents()
{
return uncomittedEvents;
}
public void ClearUncommittedEvents()
{
uncomittedEvents.Clear();
}
public virtual void ApplySnapshot(T newSnapshot)
{
snapshot = newSnapshot;
}
public virtual void ApplyEvent(Envelope<IEvent> @event)
{
}
public Task WriteSnapshotAsync()
{
snapshot.Version = persistence.Version;
return persistence.WriteSnapshotAsync(snapshot);
}
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, false);
}
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), false);
}
protected Task<object> CreateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler.ToDefault<TCommand, object>(), false);
}
protected Task<object> CreateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), false);
}
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, true);
}
protected Task<object> UpdateReturnAsync<TCommand>(TCommand command, Func<TCommand, object> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToAsync(), true);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Func<TCommand, Task> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>(), true);
}
protected Task<object> UpdateAsync<TCommand>(TCommand command, Action<TCommand> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler?.ToDefault<TCommand, object>()?.ToAsync(), true);
}
private async Task<object> InvokeAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler, bool isUpdate) where TCommand : class, IAggregateCommand
{
Guard.NotNull(command, nameof(command));
if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version)
{
throw new DomainObjectVersionException(Id.ToString(), GetType(), Version, command.ExpectedVersion);
}
if (isUpdate && Version < 0)
{
throw new DomainObjectNotFoundException(Id.ToString(), GetType());
}
else if (!isUpdate && Version >= 0)
{
throw new DomainException("Object has already been created.");
}
var previousSnapshot = snapshot;
try
{
var result = await handler(command);
var events = uncomittedEvents.ToArray();
if (events.Length > 0)
{
snapshot.Version = NewVersion;
await persistence.WriteEventsAsync(events);
await persistence.WriteSnapshotAsync(snapshot);
}
if (result == null)
{
if (isUpdate)
{
result = new EntitySavedResult(Version);
}
else
{
result = EntityCreatedResult.Create(Id, Version);
}
}
return result;
}
catch
{
snapshot = previousSnapshot;
throw;
}
finally
{
ClearUncommittedEvents();
}
}
public abstract Task<object> ExecuteAsync(IAggregateCommand command);
}
}

46
src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public class GrainCommandMiddleware<TCommand, TGrain> : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain
{
private readonly IStateFactory stateFactory;
public GrainCommandMiddleware(IStateFactory stateFactory)
{
Guard.NotNull(stateFactory, nameof(stateFactory));
this.stateFactory = stateFactory;
}
public async virtual Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.Command is TCommand typedCommand)
{
var result = await ExecuteCommandAsync(typedCommand);
context.Complete(result);
}
await next();
}
protected async Task<object> ExecuteCommandAsync(TCommand typedCommand)
{
var grain = await stateFactory.CreateAsync<TGrain>(typedCommand.AggregateId);
var result = await grain.ExecuteAsync(typedCommand);
return result;
}
}
}

23
src/Squidex.Infrastructure/Commands/IAggregateHandler.cs

@ -1,23 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Commands
{
public interface IAggregateHandler
{
Task<T> CreateAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IDomainObject;
Task<T> CreateSyncedAsync<T>(CommandContext context, Func<T, Task> creator) where T : class, IDomainObject;
Task<T> UpdateAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IDomainObject;
Task<T> UpdateSyncedAsync<T>(CommandContext context, Func<T, Task> updater) where T : class, IDomainObject;
}
}

10
src/Squidex.Infrastructure/Commands/IDomainObject.cs → src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs

@ -1,7 +1,7 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
@ -11,10 +11,12 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public interface IDomainObject : IStatefulObject<Guid>
public interface IDomainObjectGrain : IStatefulObject<Guid>
{
long Version { get; }
Task<object> ExecuteAsync(IAggregateCommand command);
Task WriteSnapshotAsync();
Task WriteAsync();
long Version { get; }
}
}

63
src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs

@ -0,0 +1,63 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Commands
{
public class SyncedGrainCommandMiddleware<TCommand, TGrain> : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain
{
private readonly AsyncLockPool lockPool = new AsyncLockPool(10000);
private readonly IStateFactory stateFactory;
public SyncedGrainCommandMiddleware(IStateFactory stateFactory)
{
Guard.NotNull(stateFactory, nameof(stateFactory));
this.stateFactory = stateFactory;
}
public async virtual Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.Command is TCommand typedCommand)
{
var result = await ExecuteCommandAsync(typedCommand);
context.Complete(result);
}
await next();
}
protected async Task<object> ExecuteCommandAsync(TCommand typedCommand)
{
var id = typedCommand.AggregateId;
using (await lockPool.LockAsync(typedCommand.AggregateId))
{
try
{
var grain = await stateFactory.GetSingleAsync<TGrain>(id);
var result = await grain.ExecuteAsync(typedCommand);
stateFactory.Synchronize<TGrain, Guid>(id);
return result;
}
catch
{
stateFactory.Remove<TGrain, Guid>(id);
throw;
}
}
}
}
}

9
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -17,6 +17,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public class EventConsumerGrain : DisposableObjectBase, IStatefulObject<string>, IEventSubscriber
{
private readonly IEventDataFormatter eventDataFormatter;
private readonly IStore<string> store;
private readonly IEventStore eventStore;
private readonly ISemanticLog log;
private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1);
@ -26,16 +27,18 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
private EventConsumerState state = new EventConsumerState();
public EventConsumerGrain(
IStore<string> store,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
Guard.NotNull(store, nameof(store));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
this.log = log;
this.store = store;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
}
@ -48,9 +51,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
}
}
public Task ActivateAsync(string key, IStore<string> store)
public Task ActivateAsync(string key)
{
persistence = store.WithSnapshots<EventConsumerState, string>(key, s => state = s);
persistence = store.WithSnapshots<EventConsumerGrain, EventConsumerState, string>(key, s => state = s);
return persistence.ReadAsync();
}

2
src/Squidex.Infrastructure/States/IStatefulObject.cs

@ -11,6 +11,6 @@ namespace Squidex.Infrastructure.States
{
public interface IStatefulObject<TKey>
{
Task ActivateAsync(TKey key, IStore<TKey> store);
Task ActivateAsync(TKey key);
}
}

6
src/Squidex.Infrastructure/States/IStore.cs

@ -13,10 +13,10 @@ namespace Squidex.Infrastructure.States
{
public interface IStore<TKey>
{
IPersistence WithEventSourcing(TKey key, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence WithEventSourcing(Type owner, TKey key, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence<T> WithSnapshots<T>(TKey key, Func<T, Task> applySnapshot);
IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot);
IPersistence<T> WithSnapshotsAndEventSourcing<T>(TKey key, Func<T, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent);
}
}

6
src/Squidex.Infrastructure/States/Persistence.cs

@ -11,15 +11,15 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
internal sealed class Persistence<TOwner, TKey> : Persistence<TOwner, object, TKey>, IPersistence
internal sealed class Persistence<TKey> : Persistence<object, TKey>, IPersistence
{
public Persistence(TKey ownerKey,
public Persistence(TKey ownerKey, Type ownerType,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISnapshotStore<object, TKey> snapshotStore,
IStreamNameResolver streamNameResolver,
Func<Envelope<IEvent>, Task> applyEvent)
: base(ownerKey, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent)
: base(ownerKey, ownerType, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent)
{
}
}

16
src/Squidex.Infrastructure/States/Persistence{TOwner,TSnapshot,TKey}.cs → src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs

@ -15,9 +15,10 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
internal class Persistence<TOwner, TSnapshot, TKey> : IPersistence<TSnapshot>
internal class Persistence<TSnapshot, TKey> : IPersistence<TSnapshot>
{
private readonly TKey ownerKey;
private readonly Type ownerType;
private readonly ISnapshotStore<TSnapshot, TKey> snapshotStore;
private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore;
@ -34,7 +35,7 @@ namespace Squidex.Infrastructure.States
get { return version; }
}
public Persistence(TKey ownerKey,
public Persistence(TKey ownerKey, Type ownerType,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISnapshotStore<TSnapshot, TKey> snapshotStore,
@ -44,6 +45,7 @@ namespace Squidex.Infrastructure.States
Func<Envelope<IEvent>, Task> applyEvent)
{
this.ownerKey = ownerKey;
this.ownerType = ownerType;
this.applyState = applyState;
this.applyEvent = applyEvent;
this.eventStore = eventStore;
@ -67,11 +69,11 @@ namespace Squidex.Infrastructure.States
{
if (version == EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(ownerKey.ToString(), typeof(TOwner));
throw new DomainObjectNotFoundException(ownerKey.ToString(), ownerType);
}
else
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), version, expectedVersion);
throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, version, expectedVersion);
}
}
}
@ -134,7 +136,7 @@ namespace Squidex.Infrastructure.States
}
catch (InconsistentStateException ex)
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, ex.CurrentVersion, ex.ExpectedVersion);
}
versionSnapshot = newVersion;
@ -164,7 +166,7 @@ namespace Squidex.Infrastructure.States
}
catch (WrongEventVersionException ex)
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
throw new DomainObjectVersionException(ownerKey.ToString(), ownerType, ex.CurrentVersion, ex.ExpectedVersion);
}
versionEvents += eventArray.Length;
@ -180,7 +182,7 @@ namespace Squidex.Infrastructure.States
private string GetStreamName()
{
return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey.ToString());
return streamNameResolver.GetStreamName(ownerType, ownerKey.ToString());
}
private bool UseSnapshots()

30
src/Squidex.Infrastructure/States/StateFactory.cs

@ -8,7 +8,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable RECS0096 // Type parameter is never used
@ -20,9 +19,6 @@ namespace Squidex.Infrastructure.States
private readonly IPubSub pubSub;
private readonly IMemoryCache statesCache;
private readonly IServiceProvider services;
private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly object lockObject = new object();
private IDisposable pubSubSubscription;
@ -31,11 +27,11 @@ namespace Squidex.Infrastructure.States
private readonly Task activationTask;
private readonly T obj;
public ObjectHolder(T obj, TKey key, IStore<TKey> store)
public ObjectHolder(T obj, TKey key)
{
this.obj = obj;
activationTask = obj.ActivateAsync(key, store);
activationTask = obj.ActivateAsync(key);
}
public async Task<T> ActivateAsync()
@ -46,27 +42,15 @@ namespace Squidex.Infrastructure.States
}
}
public StateFactory(
IPubSub pubSub,
IMemoryCache statesCache,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
IServiceProvider services,
IStreamNameResolver streamNameResolver)
public StateFactory(IPubSub pubSub, IMemoryCache statesCache, IServiceProvider services)
{
Guard.NotNull(services, nameof(services));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(pubSub, nameof(pubSub));
Guard.NotNull(services, nameof(services));
Guard.NotNull(statesCache, nameof(statesCache));
Guard.NotNull(streamNameResolver, nameof(streamNameResolver));
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.pubSub = pubSub;
this.services = services;
this.statesCache = statesCache;
this.streamNameResolver = streamNameResolver;
}
public void Initialize()
@ -94,10 +78,9 @@ namespace Squidex.Infrastructure.States
{
Guard.NotNull(key, nameof(key));
var stateStore = new Store<T, TKey>(eventStore, eventDataFormatter, services, streamNameResolver);
var state = (T)services.GetService(typeof(T));
await state.ActivateAsync(key, stateStore);
await state.ActivateAsync(key);
return state;
}
@ -124,9 +107,8 @@ namespace Squidex.Infrastructure.States
}
var state = (T)services.GetService(typeof(T));
var stateStore = new Store<T, TKey>(eventStore, eventDataFormatter, services, streamNameResolver);
stateObj = new ObjectHolder<T, TKey>(state, key, stateStore);
stateObj = new ObjectHolder<T, TKey>(state, key);
statesCache.CreateEntry(key)
.SetValue(stateObj)

18
src/Squidex.Infrastructure/States/Store.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
internal sealed class Store<TOwner, TKey> : IStore<TKey>
public sealed class Store<TKey> : IStore<TKey>
{
private readonly IServiceProvider services;
private readonly IStreamNameResolver streamNameResolver;
@ -30,32 +30,32 @@ namespace Squidex.Infrastructure.States
this.streamNameResolver = streamNameResolver;
}
public IPersistence<TState> WithSnapshots<TState>(TKey key, Func<TState, Task> applySnapshot)
public IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot)
{
return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null);
return CreatePersistence<TState>(owner, key, PersistenceMode.Snapshots, applySnapshot, null);
}
public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
return CreatePersistence<TState>(owner, key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
}
public IPersistence WithEventSourcing(TKey key, Func<Envelope<IEvent>, Task> applyEvent)
public IPersistence WithEventSourcing(Type owner, TKey key, Func<Envelope<IEvent>, Task> applyEvent)
{
Guard.NotDefault(key, nameof(key));
var snapshotStore = (ISnapshotStore<object, TKey>)services.GetService(typeof(ISnapshotStore<object, TKey>));
return new Persistence<TOwner, TKey>(key, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent);
return new Persistence<TKey>(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent);
}
private IPersistence<TState> CreatePersistence<TState>(TKey key, PersistenceMode mode, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
private IPersistence<TState> CreatePersistence<TState>(Type owner, TKey key, PersistenceMode mode, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
Guard.NotDefault(key, nameof(key));
var snapshotStore = (ISnapshotStore<TState, TKey>)services.GetService(typeof(ISnapshotStore<TState, TKey>));
return new Persistence<TOwner, TState, TKey>(key, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
return new Persistence<TState, TKey>(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
}
}
}

43
src/Squidex.Infrastructure/States/StoreExtensions.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
@ -13,19 +14,49 @@ namespace Squidex.Infrastructure.States
{
public static class StoreExtensions
{
public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent)
public static IPersistence WithEventSourcing<TOwner, TKey>(this IStore<TKey> store, TKey key, Func<Envelope<IEvent>, Task> applyEvent)
{
return store.WithEventSourcing(key, applyEvent.ToAsync());
return store.WithEventSourcing(typeof(TOwner), key, applyEvent);
}
public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot)
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot)
{
return store.WithSnapshots(key, applySnapshot.ToAsync());
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot);
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
return store.WithSnapshotsAndEventSourcing(key, applySnapshot.ToAsync(), applyEvent.ToAsync());
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot, applyEvent);
}
public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, Type owner, TKey key, Action<Envelope<IEvent>> applyEvent)
{
return store.WithEventSourcing(owner, key, applyEvent.ToAsync());
}
public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot)
{
return store.WithSnapshots<TState>(owner, key, applySnapshot.ToAsync());
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, Type owner, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TState>(owner, key, applySnapshot.ToAsync(), applyEvent.ToAsync());
}
public static IPersistence WithEventSourcing<TOwner, TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent)
{
return store.WithEventSourcing(typeof(TOwner), key, applyEvent.ToAsync());
}
public static IPersistence<TState> WithSnapshots<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot)
{
return store.WithSnapshots<TState>(typeof(TOwner), key, applySnapshot.ToAsync());
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TState>(typeof(TOwner), key, applySnapshot.ToAsync(), applyEvent.ToAsync());
}
}
}

38
src/Squidex.Infrastructure/Tasks/TaskExtensions.cs

@ -16,7 +16,43 @@ namespace Squidex.Infrastructure.Tasks
{
}
public static Func<T, Task> ToAsync<T>(this Action<T> action)
public static Func<TInput, TOutput> ToDefault<TInput, TOutput>(this Action<TInput> action)
{
Guard.NotNull(action, nameof(action));
return x =>
{
action(x);
return default(TOutput);
};
}
public static Func<TInput, Task<TOutput>> ToDefault<TInput, TOutput>(this Func<TInput, Task> action)
{
Guard.NotNull(action, nameof(action));
return async x =>
{
await action(x);
return default(TOutput);
};
}
public static Func<TInput, Task<TOutput>> ToAsync<TInput, TOutput>(this Func<TInput, TOutput> action)
{
Guard.NotNull(action, nameof(action));
return x =>
{
var result = action(x);
return Task.FromResult(result);
};
}
public static Func<TInput, Task> ToAsync<TInput>(this Action<TInput> action)
{
return x =>
{

3
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -77,9 +77,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ActionContextAccessor>()
.As<IActionContextAccessor>();
services.AddSingletonAs<AggregateHandler>()
.As<IAggregateHandler>();
services.AddSingletonAs<InMemoryCommandBus>()
.As<ICommandBus>();

22
src/Squidex/Config/Domain/WriteServices.cs

@ -12,11 +12,15 @@ using Migrate_01;
using Migrate_01.Migrations;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Commands;
@ -50,19 +54,19 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<EnrichWithSchemaIdCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AppCommandMiddleware>()
services.AddSingletonAs<SyncedGrainCommandMiddleware<AppCommand, AppGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<ContentCommandMiddleware>()
services.AddSingletonAs<GrainCommandMiddleware<ContentCommand, ContentGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<SchemaCommandMiddleware>()
services.AddSingletonAs<SyncedGrainCommandMiddleware<SchemaCommand, SchemaGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<RuleCommandMiddleware>()
services.AddSingletonAs<SyncedGrainCommandMiddleware<RuleCommand, RuleGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<CreateBlogCommandMiddleware>()
@ -92,19 +96,19 @@ namespace Squidex.Config.Domain
services.AddTransientAs<Rebuilder>()
.AsSelf();
services.AddTransientAs<AppDomainObject>()
services.AddTransientAs<AppGrain>()
.AsSelf();
services.AddTransientAs<AssetDomainObject>()
services.AddTransientAs<AssetGrain>()
.AsSelf();
services.AddTransientAs<ContentDomainObject>()
services.AddTransientAs<ContentGrain>()
.AsSelf();
services.AddTransientAs<RuleDomainObject>()
services.AddTransientAs<RuleGrain>()
.AsSelf();
services.AddTransientAs<SchemaDomainObject>()
services.AddTransientAs<SchemaGrain>()
.AsSelf();
services.AddSingleton<InitialPatterns>(c =>

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

@ -1,291 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
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.Infrastructure.States;
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 Language language = Language.DE;
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientName = "client";
private readonly Guid patternId = Guid.NewGuid();
private readonly AppDomainObject app = new AppDomainObject(new InitialPatterns());
private readonly AppCommandMiddleware sut;
protected override Guid Id
{
get { return AppId; }
}
public AppCommandMiddlewareTests()
{
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);
app.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[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.Snapshot.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);
});
}
[Fact]
public async Task AddPattern_should_update_domain_object()
{
CreateApp();
var context = CreateContextForCommand(new AddPattern { Name = "Any", Pattern = ".*" });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task UpdatePattern_should_update_domain()
{
CreateApp()
.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." }));
var context = CreateContextForCommand(new UpdatePattern { PatternId = patternId, Name = "Number", Pattern = "[0-9]" });
await TestUpdate(app, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DeletePattern_should_update_domain_object()
{
CreateApp()
.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = "." }));
var context = CreateContextForCommand(new DeletePattern { PatternId = patternId });
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")));
}
}
}

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

@ -1,399 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
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 Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppDomainObjectTests : HandlerTestBase<AppDomainObject>
{
private readonly string contributorId = Guid.NewGuid().ToString();
private readonly string clientId = "client";
private readonly string clientNewName = "My Client";
private readonly string planId = "premium";
private readonly Guid patternId = Guid.NewGuid();
private readonly AppDomainObject sut = new AppDomainObject(new InitialPatterns());
protected override Guid Id
{
get { return AppId; }
}
public AppDomainObjectTests()
{
sut.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[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()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
var initialPatterns = new InitialPatterns
{
{ id1, new AppPattern("Number", "[0-9]") },
{ id2, new AppPattern("Numbers", "[0-9]*") }
};
var app = new AppDomainObject(initialPatterns);
app.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId }));
Assert.Equal(AppName, app.Snapshot.Name);
app.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }),
CreateEvent(new AppLanguageAdded { Language = Language.EN }),
CreateEvent(new AppPatternAdded { PatternId = id1, Name = "Number", Pattern = "[0-9]" }),
CreateEvent(new AppPatternAdded { PatternId = id2, Name = "Numbers", Pattern = "[0-9]*" })
);
}
[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 }));
Assert.Equal(planId, sut.Snapshot.Plan.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 }));
Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]);
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 }));
Assert.False(sut.Snapshot.Contributors.ContainsKey(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));
Assert.True(sut.Snapshot.Clients.ContainsKey(clientId));
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 }));
Assert.False(sut.Snapshot.Clients.ContainsKey(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 }));
Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name);
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 }));
Assert.True(sut.Snapshot.LanguagesConfig.Contains(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 }));
Assert.False(sut.Snapshot.LanguagesConfig.Contains(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 } }));
Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } })
);
}
[Fact]
public void AddPattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() => sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*" })));
}
[Fact]
public void AddPattern_should_create_events()
{
CreateApp();
sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }));
Assert.Single(sut.Snapshot.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternAdded { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
[Fact]
public void DeletePattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.DeletePattern(CreateCommand(new DeletePattern
{
PatternId = Guid.NewGuid()
}));
});
}
[Fact]
public void DeletePattern_should_create_events()
{
CreateApp();
CreatePattern();
sut.DeletePattern(CreateCommand(new DeletePattern { PatternId = patternId }));
Assert.Empty(sut.Snapshot.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternDeleted { PatternId = patternId })
);
}
[Fact]
public void UpdatePattern_should_throw_exception_if_app_not_created()
{
Assert.Throws<DomainException>(() => sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*" })));
}
[Fact]
public void UpdatePattern_should_create_events()
{
CreateApp();
CreatePattern();
sut.UpdatePattern(CreateCommand(new UpdatePattern { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" }));
Assert.Single(sut.Snapshot.Patterns);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new AppPatternUpdated { PatternId = patternId, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
private void CreatePattern()
{
sut.AddPattern(CreateCommand(new AddPattern { PatternId = patternId, Name = "Name", Pattern = ".*" }));
sut.ClearUncommittedEvents();
}
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();
}
}
}

388
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -0,0 +1,388 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
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.Domain.Apps.Entities.Apps.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Shared.Users;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppGrainTests : HandlerTestBase<AppGrain, AppState>
{
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 string contributorId = Guid.NewGuid().ToString();
private readonly string clientId = "client";
private readonly string clientNewName = "My Client";
private readonly string planId = "premium";
private readonly AppGrain sut;
private readonly Guid patternId1 = Guid.NewGuid();
private readonly Guid patternId2 = Guid.NewGuid();
private readonly Guid patternId3 = Guid.NewGuid();
private readonly InitialPatterns initialPatterns;
protected override Guid Id
{
get { return AppId; }
}
public AppGrainTests()
{
A.CallTo(() => appProvider.GetAppAsync(AppName))
.Returns((IAppEntity)null);
A.CallTo(() => userResolver.FindByIdAsync(contributorId))
.Returns(A.Fake<IUser>());
initialPatterns = new InitialPatterns
{
{ patternId1, new AppPattern("Number", "[0-9]") },
{ patternId2, new AppPattern("Numbers", "[0-9]*") }
};
sut = new AppGrain(initialPatterns, Store, appProvider, appPlansProvider, appPlansBillingManager, userResolver);
sut.ActivateAsync(Id).Wait();
}
[Fact]
public async Task Create_should_create_events_and_update_state()
{
var command = new CreateApp { Name = AppName, Actor = User, AppId = AppId };
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 4));
Assert.Equal(AppName, sut.Snapshot.Name);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppCreated { Name = AppName }),
CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }),
CreateEvent(new AppLanguageAdded { Language = Language.EN }),
CreateEvent(new AppPatternAdded { PatternId = patternId1, Name = "Number", Pattern = "[0-9]" }),
CreateEvent(new AppPatternAdded { PatternId = patternId2, Name = "Numbers", Pattern = "[0-9]*" })
);
}
[Fact]
public async Task ChangePlan_should_create_events_and_update_state()
{
var command = new ChangePlan { PlanId = planId };
A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId))
.Returns(true);
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId))
.Returns(new PlanChangedResult());
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
Assert.True(result is PlanChangedResult);
Assert.Equal(planId, sut.Snapshot.Plan.PlanId);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPlanChanged { PlanId = planId })
);
}
[Fact]
public async Task ChangePlan_should_not_make_update_for_redirect_result()
{
var command = new ChangePlan { PlanId = planId };
A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId))
.Returns(true);
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId))
.Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io")));
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new RedirectToCheckoutResult(new Uri("http://squidex.io")));
Assert.Null(sut.Snapshot.Plan);
}
[Fact]
public async Task ChangePlan_should_not_call_billing_manager_for_callback()
{
var command = new ChangePlan { PlanId = planId, FromCallback = true };
A.CallTo(() => appPlansProvider.IsConfiguredPlan(planId))
.Returns(true);
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
A.CallTo(() => appPlansBillingManager.ChangePlanAsync(User.Identifier, AppId, AppName, planId))
.MustNotHaveHappened();
}
[Fact]
public async Task AssignContributor_should_create_events_and_update_state()
{
var command = new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
Assert.Equal(AppContributorPermission.Editor, sut.Snapshot.Contributors[contributorId]);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor })
);
}
[Fact]
public async Task RemoveContributor_should_create_events_and_update_state()
{
var command = new RemoveContributor { ContributorId = contributorId };
await ExecuteCreateAsync();
await ExecuteAssignContributorAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
Assert.False(sut.Snapshot.Contributors.ContainsKey(contributorId));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppContributorRemoved { ContributorId = contributorId })
);
}
[Fact]
public async Task AttachClient_should_create_events_and_update_state()
{
var command = new AttachClient { Id = clientId };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
Assert.True(sut.Snapshot.Clients.ContainsKey(clientId));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret })
);
}
[Fact]
public async Task RevokeClient_should_create_events_and_update_state()
{
var command = new RevokeClient { Id = clientId };
await ExecuteCreateAsync();
await ExecuteAttachClientAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
Assert.False(sut.Snapshot.Clients.ContainsKey(clientId));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppClientRevoked { Id = clientId })
);
}
[Fact]
public async Task UpdateClient_should_create_events_and_update_state()
{
var command = new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer };
await ExecuteCreateAsync();
await ExecuteAttachClientAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(7));
Assert.Equal(clientNewName, sut.Snapshot.Clients[clientId].Name);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }),
CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer })
);
}
[Fact]
public async Task AddLanguage_should_create_events_and_update_state()
{
var command = new AddLanguage { Language = Language.DE };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppLanguageAdded { Language = Language.DE })
);
}
[Fact]
public async Task RemoveLanguage_should_create_events_and_update_state()
{
var command = new RemoveLanguage { Language = Language.DE };
await ExecuteCreateAsync();
await ExecuteAddLanguageAsync(Language.DE);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
Assert.False(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppLanguageRemoved { Language = Language.DE })
);
}
[Fact]
public async Task UpdateLanguage_should_create_events_and_update_state()
{
var command = new UpdateLanguage { Language = Language.DE, Fallback = new List<Language> { Language.EN } };
await ExecuteCreateAsync();
await ExecuteAddLanguageAsync(Language.DE);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
Assert.True(sut.Snapshot.LanguagesConfig.Contains(Language.DE));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List<Language> { Language.EN } })
);
}
[Fact]
public async Task AddPattern_should_create_events_and_update_state()
{
var command = new AddPattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(5));
Assert.Equal(initialPatterns.Count + 1, sut.Snapshot.Patterns.Count);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternAdded { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
[Fact]
public async Task DeletePattern_should_create_events_and_update_state()
{
var command = new DeletePattern { PatternId = patternId3 };
await ExecuteCreateAsync();
await ExecuteAddPatternAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
Assert.Equal(initialPatterns.Count, sut.Snapshot.Patterns.Count);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternDeleted { PatternId = patternId3 })
);
}
[Fact]
public async Task UpdatePattern_should_create_events_and_update_state()
{
var command = new UpdatePattern { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" };
await ExecuteCreateAsync();
await ExecuteAddPatternAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(6));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppPatternUpdated { PatternId = patternId3, Name = "Any", Pattern = ".*", Message = "Msg" })
);
}
private Task ExecuteAddPatternAsync()
{
return sut.ExecuteAsync(CreateCommand(new AddPattern { PatternId = patternId3, Name = "Name", Pattern = ".*" }));
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateCommand(new CreateApp { Name = AppName }));
}
private Task ExecuteAssignContributorAsync()
{
return sut.ExecuteAsync(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor }));
}
private Task ExecuteAttachClientAsync()
{
return sut.ExecuteAsync(CreateCommand(new AttachClient { Id = clientId }));
}
private Task ExecuteAddLanguageAsync(Language language)
{
return sut.ExecuteAsync(CreateCommand(new AddLanguage { Language = language }));
}
}
}

2
tests/Squidex.Domain.Apps.Entities.Tests/Apps/ConfigAppLimitsProviderTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/ConfigAppLimitsProviderTests.cs

@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Services.Implementations;
using Squidex.Infrastructure;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps
namespace Squidex.Domain.Apps.Entities.Apps.Billing
{
public class ConfigAppLimitsProviderTests
{

81
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -10,22 +10,25 @@ using System.IO;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetDomainObject>
public class AssetCommandMiddlewareTests : HandlerTestBase<AssetGrain, AssetState>
{
private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake<IAssetThumbnailGenerator>();
private readonly IAssetStore assetStore = A.Fake<IAssetStore>();
private readonly IStateFactory stateFactory = A.Fake<IStateFactory>();
private readonly Guid assetId = Guid.NewGuid();
private readonly Stream stream = new MemoryStream();
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly AssetDomainObject asset = new AssetDomainObject();
private readonly AssetGrain asset;
private readonly AssetFile file;
private readonly AssetCommandMiddleware sut;
@ -38,7 +41,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator);
asset = new AssetGrain(Store);
asset.ActivateAsync(Id).Wait();
A.CallTo(() => stateFactory.CreateAsync<AssetGrain>(Id))
.Returns(asset);
sut = new AssetCommandMiddleware(stateFactory, assetStore, assetThumbnailGenerator);
}
[Fact]
@ -49,10 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupStore(0, context.ContextId);
SetupImageInfo();
await TestCreate(asset, async _ =>
{
await sut.HandleAsync(context);
});
await sut.HandleAsync(context);
Assert.Equal(assetId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
@ -68,52 +74,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
SetupStore(1, context.ContextId);
SetupImageInfo();
CreateAsset();
await ExecuteCreateAsync();
await TestUpdate(asset, async _ =>
{
await sut.HandleAsync(context);
});
await sut.HandleAsync(context);
AssertAssetHasBeenUploaded(1, context.ContextId);
AssertAssetImageChecked();
}
[Fact]
public async Task Rename_should_update_domain_object()
{
CreateAsset();
var context = CreateContextForCommand(new RenameAsset { AssetId = assetId, FileName = "my-new-image.png" });
await TestUpdate(asset, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Delete_should_update_domain_object()
private Task ExecuteCreateAsync()
{
CreateAsset();
var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId });
await TestUpdate(asset, async _ =>
{
await sut.HandleAsync(command);
});
}
private void CreateAsset()
{
asset.Create(CreateCommand(new CreateAsset { File = file }));
}
private void SetupImageInfo()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.Returns(image);
return asset.ExecuteAsync(CreateCommand(new CreateAsset { AssetId = Id, File = file }));
}
private void SetupStore(long version, Guid commitId)
@ -126,12 +97,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
.Returns(TaskHelper.Done);
}
private void AssertAssetImageChecked()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.MustHaveHappened();
}
private void AssertAssetHasBeenUploaded(long version, Guid commitId)
{
A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream))
@ -141,5 +106,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString()))
.MustHaveHappened();
}
private void SetupImageInfo()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.Returns(image);
}
private void AssertAssetImageChecked()
{
A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream))
.MustHaveHappened();
}
}
}

220
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs

@ -1,220 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetDomainObjectTests : HandlerTestBase<AssetDomainObject>
{
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly Guid assetId = Guid.NewGuid();
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly AssetDomainObject sut = new AssetDomainObject();
protected override Guid Id
{
get { return assetId; }
}
public AssetDomainObjectTests()
{
sut.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[Fact]
public void Create_should_throw_exception_if_created()
{
CreateAsset();
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
});
}
[Fact]
public void Create_should_create_events()
{
sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image }));
Assert.Equal(0, sut.Snapshot.FileVersion);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetCreated
{
IsImage = true,
FileName = file.FileName,
FileSize = file.FileSize,
FileVersion = 0,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateAssetCommand(new UpdateAsset { File = file }));
});
}
[Fact]
public void Update_should_throw_exception_if_asset_is_deleted()
{
CreateAsset();
DeleteAsset();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateAssetCommand(new UpdateAsset()));
});
}
[Fact]
public void Update_should_create_events()
{
CreateAsset();
sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image }));
Assert.Equal(1, sut.Snapshot.FileVersion);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetUpdated
{
IsImage = true,
FileSize = file.FileSize,
FileVersion = 1,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
[Fact]
public void Rename_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" }));
});
}
[Fact]
public void Rename_should_throw_exception_if_asset_is_deleted()
{
CreateAsset();
DeleteAsset();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateAssetCommand(new UpdateAsset()));
});
}
[Fact]
public void Rename_should_create_events()
{
CreateAsset();
sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" }));
Assert.Equal("my-new-image.png", sut.Snapshot.FileName);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" })
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateAssetCommand(new DeleteAsset()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateAsset();
DeleteAsset();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateAssetCommand(new DeleteAsset()));
});
}
[Fact]
public void Delete_should_create_events_with_total_file_size()
{
CreateAsset();
UpdateAsset();
sut.Delete(CreateAssetCommand(new DeleteAsset()));
Assert.True(sut.Snapshot.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 })
);
}
private void CreateAsset()
{
sut.Create(CreateAssetCommand(new CreateAsset { File = file }));
sut.ClearUncommittedEvents();
}
private void UpdateAsset()
{
sut.Update(CreateAssetCommand(new UpdateAsset { File = file }));
sut.ClearUncommittedEvents();
}
private void DeleteAsset()
{
sut.Delete(CreateAssetCommand(new DeleteAsset()));
sut.ClearUncommittedEvents();
}
protected T CreateAssetEvent<T>(T @event) where T : AssetEvent
{
@event.AssetId = assetId;
return CreateEvent(@event);
}
protected T CreateAssetCommand<T>(T command) where T : AssetCommand
{
command.AssetId = assetId;
return CreateCommand(command);
}
}
}

170
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -0,0 +1,170 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetGrainTests : HandlerTestBase<AssetGrain, AssetState>
{
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly Guid assetId = Guid.NewGuid();
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly AssetGrain sut;
protected override Guid Id
{
get { return assetId; }
}
public AssetGrainTests()
{
sut = new AssetGrain(Store);
sut.ActivateAsync(Id).Wait();
}
[Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
await Assert.ThrowsAsync<DomainException>(ExecuteUpdateAsync);
}
[Fact]
public async Task Create_should_create_events()
{
var command = new CreateAsset { File = file, ImageInfo = image };
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new AssetSavedResult(0, 0));
Assert.Equal(0, sut.Snapshot.FileVersion);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetCreated
{
IsImage = true,
FileName = file.FileName,
FileSize = file.FileSize,
FileVersion = 0,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
[Fact]
public async Task Update_should_create_events()
{
var command = new UpdateAsset { File = file, ImageInfo = image };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new AssetSavedResult(1, 1));
Assert.Equal(1, sut.Snapshot.FileVersion);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetUpdated
{
IsImage = true,
FileSize = file.FileSize,
FileVersion = 1,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
PixelHeight = image.PixelHeight
})
);
}
[Fact]
public async Task Rename_should_create_events()
{
var command = new RenameAsset { FileName = "my-new-image.png" };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal("my-new-image.png", sut.Snapshot.FileName);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" })
);
}
[Fact]
public async Task Delete_should_create_events_with_total_file_size()
{
var command = new DeleteAsset();
await ExecuteCreateAsync();
await ExecuteUpdateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.True(sut.Snapshot.IsDeleted);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 })
);
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateAssetCommand(new CreateAsset { File = file }));
}
private Task ExecuteUpdateAsync()
{
return sut.ExecuteAsync(CreateAssetCommand(new UpdateAsset { File = file }));
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateAssetCommand(new DeleteAsset()));
}
protected T CreateAssetEvent<T>(T @event) where T : AssetEvent
{
@event.AssetId = assetId;
return CreateEvent(@event);
}
protected T CreateAssetCommand<T>(T command) where T : AssetCommand
{
command.AssetId = assetId;
return CreateCommand(command);
}
}
}

264
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentCommandMiddlewareTests.cs

@ -1,264 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
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.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentCommandMiddlewareTests : HandlerTestBase<ContentDomainObject>
{
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentDomainObject content = new ContentDomainObject();
private readonly ContentCommandMiddleware sut;
protected override Guid Id
{
get { return contentId; }
}
private readonly NamedContentData invalidData =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(null))
.AddField("my-field2", new ContentFieldData()
.AddValue(1));
private readonly NamedContentData data =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(1))
.AddField("my-field2", new ContentFieldData()
.AddValue(1));
private readonly NamedContentData patch =
new NamedContentData()
.AddField("my-field1", new ContentFieldData()
.AddValue(1));
public ContentCommandMiddlewareTests()
{
var schemaDef =
new Schema("my-schema")
.AddField(new NumberField(1, "my-field1", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true }))
.AddField(new NumberField(2, "my-field2", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = false }));
sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy<IAssetRepository>(), scriptEngine, A.Dummy<IContentRepository>());
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
A.CallTo(() => schema.ScriptCreate).Returns("<create-script>");
A.CallTo(() => schema.ScriptChange).Returns("<change-script>");
A.CallTo(() => schema.ScriptUpdate).Returns("<update-script>");
A.CallTo(() => schema.ScriptDelete).Returns("<delete-script>");
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema));
}
[Fact]
public async Task Create_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user });
await TestCreate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Create_should_create_content()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user });
await TestCreate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustNotHaveHappened();
}
[Fact]
public async Task Create_should_also_invoke_publish_script_when_publishing()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user, Publish = true });
await TestCreate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<EntityCreatedResult<NamedContentData>>().IdOrValue);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>")).MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened();
}
[Fact]
public async Task Update_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
CreateContent();
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user });
await TestUpdate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Update_should_update_domain_object()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
CreateContent();
var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(data, context.Result<ContentDataChangedResult>().Data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened();
}
[Fact]
public async Task Patch_should_throw_exception_if_data_is_not_valid()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(invalidData);
CreateContent();
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user });
await TestUpdate(content, async _ =>
{
await Assert.ThrowsAsync<ValidationException>(() => sut.HandleAsync(context));
}, false);
}
[Fact]
public async Task Patch_should_update_domain_object()
{
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored))
.Returns(data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, A<string>.Ignored)).Returns(patch);
CreateContent();
var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
Assert.NotNull(context.Result<ContentDataChangedResult>().Data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>")).MustHaveHappened();
}
[Fact]
public async Task ChangeStatus_should_publish_domain_object()
{
CreateContent();
var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustHaveHappened();
}
[Fact]
public async Task ChangeStatus_should_not_invoke_scripts_when_scheduled()
{
CreateContent();
var context = CreateContextForCommand(new ChangeContentStatus { ContentId = contentId, User = user, Status = Status.Published, DueTime = Instant.MaxValue });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(context);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>")).MustNotHaveHappened();
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateContent();
var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user });
await TestUpdate(content, async _ =>
{
await sut.HandleAsync(command);
});
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<delete-script>")).MustHaveHappened();
}
private void CreateContent()
{
content.Create(CreateCommand(new CreateContent { Data = data }));
}
}
}

304
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -1,304 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using FakeItEasy;
using FluentAssertions;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentDomainObjectTests : HandlerTestBase<ContentDomainObject>
{
private readonly NamedContentData data =
new NamedContentData()
.AddField("field1",
new ContentFieldData()
.AddValue("iv", 1));
private readonly NamedContentData otherData =
new NamedContentData()
.AddField("field2",
new ContentFieldData()
.AddValue("iv", 2));
private readonly NamedContentData patched;
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentDomainObject sut = new ContentDomainObject();
protected override Guid Id
{
get { return contentId; }
}
public ContentDomainObjectTests()
{
patched = otherData.MergeInto(data);
sut.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(CreateCommand(new CreateContent { Data = data }));
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
});
}
[Fact]
public void Create_should_create_events()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
}
[Fact]
public void Create_should_also_publish_if_set_to_true()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data, Publish = true }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
});
}
[Fact]
public void Update_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateContentCommand(new UpdateContent()));
});
}
[Fact]
public void Update_should_create_events()
{
CreateContent();
sut.Update(CreateContentCommand(new UpdateContent { Data = otherData }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData })
);
}
[Fact]
public void Update_should_not_create_event_for_same_data()
{
CreateContent();
UpdateContent();
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
sut.GetUncomittedEvents().Should().BeEmpty();
}
[Fact]
public void Patch_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Patch(CreateContentCommand(new PatchContent { Data = data }));
});
}
[Fact]
public void Patch_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Patch(CreateContentCommand(new PatchContent()));
});
}
[Fact]
public void Patch_should_create_events()
{
CreateContent();
UpdateContent();
sut.Patch(CreateContentCommand(new PatchContent { Data = otherData }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = patched })
);
}
[Fact]
public void Patch_should_not_create_event_for_same_data()
{
CreateContent();
UpdateContent();
sut.Patch(CreateContentCommand(new PatchContent { Data = data }));
sut.GetUncomittedEvents().Should().BeEmpty();
}
[Fact]
public void ChangeStatus_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void ChangeStatus_should_throw_exception_if_content_is_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus()));
});
}
[Fact]
public void ChangeStatus_should_refresh_properties_and_create_events()
{
CreateContent();
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published }));
Assert.Equal(Status.Published, sut.Snapshot.Status);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
}
[Fact]
public void ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time()
{
CreateContent();
var dueTime = Instant.MaxValue;
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published, DueTime = dueTime }));
Assert.Equal(Status.Draft, sut.Snapshot.Status);
Assert.Equal(Status.Published, sut.Snapshot.ScheduledTo);
Assert.Equal(dueTime, sut.Snapshot.ScheduledAt);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime })
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateContentCommand(new DeleteContent()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateContent();
DeleteContent();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateContentCommand(new DeleteContent()));
});
}
[Fact]
public void Delete_should_update_properties_and_create_events()
{
CreateContent();
sut.Delete(CreateContentCommand(new DeleteContent()));
Assert.True(sut.Snapshot.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateContentEvent(new ContentDeleted())
);
}
private void CreateContent()
{
sut.Create(CreateContentCommand(new CreateContent { Data = data }));
sut.ClearUncommittedEvents();
}
private void UpdateContent()
{
sut.Update(CreateContentCommand(new UpdateContent { Data = data }));
sut.ClearUncommittedEvents();
}
private void ChangeStatus(Status status)
{
sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status }));
sut.ClearUncommittedEvents();
}
private void DeleteContent()
{
sut.Delete(CreateContentCommand(new DeleteContent()));
sut.ClearUncommittedEvents();
}
protected T CreateContentEvent<T>(T @event) where T : ContentEvent
{
@event.ContentId = contentId;
return CreateEvent(@event);
}
protected T CreateContentCommand<T>(T command) where T : ContentCommand
{
command.ContentId = contentId;
return CreateCommand(command);
}
}
}

361
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -0,0 +1,361 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
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.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentGrainTests : HandlerTestBase<ContentGrain, ContentState>
{
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE);
private readonly NamedContentData invalidData =
new NamedContentData()
.AddField("my-field1",
new ContentFieldData()
.AddValue(null))
.AddField("my-field2",
new ContentFieldData()
.AddValue(1));
private readonly NamedContentData data =
new NamedContentData()
.AddField("my-field1",
new ContentFieldData()
.AddValue("iv", 1));
private readonly NamedContentData patch =
new NamedContentData()
.AddField("my-field2",
new ContentFieldData()
.AddValue("iv", 2));
private readonly NamedContentData otherData =
new NamedContentData()
.AddField("my-field1",
new ContentFieldData()
.AddValue("iv", 2))
.AddField("my-field2",
new ContentFieldData()
.AddValue("iv", 2));
private readonly NamedContentData patched;
private readonly Guid contentId = Guid.NewGuid();
private readonly ContentGrain sut;
protected override Guid Id
{
get { return contentId; }
}
public ContentGrainTests()
{
var schemaDef =
new Schema("my-schema")
.AddField(new NumberField(1, "my-field1", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true }))
.AddField(new NumberField(2, "my-field2", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = false }));
A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig);
A.CallTo(() => appProvider.GetAppAsync(AppName)).Returns(app);
A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId)).Returns((app, schema));
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
A.CallTo(() => schema.ScriptCreate).Returns("<create-script>");
A.CallTo(() => schema.ScriptChange).Returns("<change-script>");
A.CallTo(() => schema.ScriptUpdate).Returns("<update-script>");
A.CallTo(() => schema.ScriptDelete).Returns("<delete-script>");
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.That.Matches(x => ReferenceEquals(x.Data, data)), A<string>.Ignored))
.Returns(data);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.That.Matches(x => ReferenceEquals(x.Data, invalidData)), A<string>.Ignored))
.Returns(invalidData);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.That.Matches(x => ReferenceEquals(x.Data, patch)), A<string>.Ignored))
.Returns(patch);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.That.Matches(x => ReferenceEquals(x.Data, otherData)), A<string>.Ignored))
.Returns(otherData);
patched = patch.MergeInto(data);
sut = new ContentGrain(Store, appProvider, A.Dummy<IAssetRepository>(), scriptEngine, A.Dummy<IContentRepository>());
sut.ActivateAsync(Id).Wait();
}
[Fact]
public async Task Command_should_throw_exception_if_content_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
await Assert.ThrowsAsync<DomainException>(ExecuteUpdateAsync);
}
[Fact]
public async Task Create_should_create_events_and_update_state()
{
var command = new CreateContent { Data = data };
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(data, 0));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>"))
.MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
.MustNotHaveHappened();
}
[Fact]
public async Task Create_should_also_publish()
{
var command = new CreateContent { Data = data, Publish = true };
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(data, 1));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data }),
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<create-script>"))
.MustHaveHappened();
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
.MustHaveHappened();
}
[Fact]
public async Task Create_should_throw_when_invalid_data_is_passed()
{
var command = new CreateContent { Data = invalidData };
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(CreateContentCommand(command)));
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var command = new UpdateContent { Data = otherData };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new ContentDataChangedResult(otherData, 1));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = otherData })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustHaveHappened();
}
[Fact]
public async Task Update_should_not_create_event_for_same_data()
{
var command = new UpdateContent { Data = data };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new ContentDataChangedResult(data, 0));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustHaveHappened();
}
[Fact]
public async Task Update_should_throw_when_invalid_data_is_passed()
{
var command = new UpdateContent { Data = invalidData };
await ExecuteCreateAsync();
await Assert.ThrowsAsync<ValidationException>(() => sut.ExecuteAsync(CreateContentCommand(command)));
}
[Fact]
public async Task Patch_should_create_events_and_update_state()
{
var command = new PatchContent { Data = patch };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new ContentDataChangedResult(otherData, 1));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentUpdated { Data = patched })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustHaveHappened();
}
[Fact]
public async Task Patch_should_not_create_event_for_same_data()
{
var command = new PatchContent { Data = data };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new ContentDataChangedResult(data, 0));
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentCreated { Data = data })
);
A.CallTo(() => scriptEngine.ExecuteAndTransform(A<ScriptContext>.Ignored, "<update-script>"))
.MustHaveHappened();
}
[Fact]
public async Task ChangedStatus_should_create_events_and_update_state()
{
var command = new ChangeContentStatus { Status = Status.Published };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal(Status.Published, sut.Snapshot.Status);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusChanged { Status = Status.Published })
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
.MustHaveHappened();
}
[Fact]
public async Task ChangeStatus_should_refresh_properties_and_create_scheduled_events_when_command_has_due_time()
{
var dueTime = Instant.MaxValue;
var command = new ChangeContentStatus { Status = Status.Published, DueTime = dueTime };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal(Status.Draft, sut.Snapshot.Status);
Assert.Equal(Status.Published, sut.Snapshot.ScheduledTo);
Assert.Equal(dueTime, sut.Snapshot.ScheduledAt);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentStatusScheduled { Status = Status.Published, DueTime = dueTime })
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<change-script>"))
.MustNotHaveHappened();
}
[Fact]
public async Task Delete_should_update_properties_and_create_events()
{
var command = new DeleteContent();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateContentCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted);
LastEvents
.ShouldHaveSameEvents(
CreateContentEvent(new ContentDeleted())
);
A.CallTo(() => scriptEngine.Execute(A<ScriptContext>.Ignored, "<delete-script>"))
.MustHaveHappened();
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateContentCommand(new CreateContent { Data = data }));
}
private Task ExecuteUpdateAsync()
{
return sut.ExecuteAsync(CreateContentCommand(new UpdateContent { Data = data }));
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateContentCommand(new DeleteContent()));
}
protected T CreateContentEvent<T>(T @event) where T : ContentEvent
{
@event.ContentId = contentId;
return CreateEvent(@event);
}
protected T CreateContentCommand<T>(T command) where T : ContentCommand
{
command.ContentId = contentId;
return CreateCommand(command);
}
}
}

118
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleCommandMiddlewareTests.cs

@ -1,118 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
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.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleCommandMiddlewareTests : HandlerTestBase<RuleDomainObject>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly RuleDomainObject rule = new RuleDomainObject();
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger();
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") };
private readonly Guid ruleId = Guid.NewGuid();
private readonly RuleCommandMiddleware sut;
protected override Guid Id
{
get { return ruleId; }
}
public RuleCommandMiddlewareTests()
{
A.CallTo(() => appProvider.GetSchemaAsync(A<Guid>.Ignored, A<Guid>.Ignored, false))
.Returns(A.Fake<ISchemaEntity>());
sut = new RuleCommandMiddleware(Handler, appProvider);
}
[Fact]
public async Task Create_should_create_domain_object()
{
var context = CreateContextForCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction });
await TestCreate(rule, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Update_should_update_domain_object()
{
var context = CreateContextForCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction });
CreateRule();
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Enable_should_update_domain_object()
{
CreateRule();
DisableRule();
var command = CreateContextForCommand(new EnableRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
[Fact]
public async Task Disable_should_update_domain_object()
{
CreateRule();
var command = CreateContextForCommand(new DisableRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
[Fact]
public async Task Delete_should_update_domain_object()
{
CreateRule();
var command = CreateContextForCommand(new DeleteRule { RuleId = ruleId });
await TestUpdate(rule, async _ =>
{
await sut.HandleAsync(command);
});
}
private void DisableRule()
{
rule.Disable(CreateCommand(new DisableRule()));
}
private void CreateRule()
{
rule.Create(CreateCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
}
}
}

256
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs

@ -1,256 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Immutable;
using FakeItEasy;
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.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleDomainObjectTests : HandlerTestBase<RuleDomainObject>
{
private readonly Guid ruleId = Guid.NewGuid();
private readonly RuleTrigger ruleTrigger = new ContentChangedTrigger();
private readonly RuleAction ruleAction = new WebhookAction { Url = new Uri("https://squidex.io") };
private readonly RuleDomainObject sut = new RuleDomainObject();
protected override Guid Id
{
get { return ruleId; }
}
public RuleDomainObjectTests()
{
sut.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Create_should_create_events()
{
var command = new CreateRule { Trigger = ruleTrigger, Action = ruleAction };
sut.Create(CreateRuleCommand(command));
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Same(ruleTrigger, sut.Snapshot.RuleDef.Trigger);
Assert.Same(ruleAction, sut.Snapshot.RuleDef.Action);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleCreated { Trigger = ruleTrigger, Action = ruleAction })
);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Update_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateRuleCommand(new UpdateRule { Trigger = ruleTrigger, Action = ruleAction }));
});
}
[Fact]
public void Update_should_create_events()
{
var newTrigger = new ContentChangedTrigger
{
Schemas = ImmutableList<ContentChangedTriggerSchema>.Empty
};
var newAction = new WebhookAction
{
Url = new Uri("https://squidex.io/v2")
};
CreateRule();
var command = new UpdateRule { Trigger = newTrigger, Action = newAction };
sut.Update(CreateRuleCommand(command));
Assert.Same(newTrigger, sut.Snapshot.RuleDef.Trigger);
Assert.Same(newAction, sut.Snapshot.RuleDef.Action);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleUpdated { Trigger = newTrigger, Action = newAction })
);
}
[Fact]
public void Enable_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Enable(CreateRuleCommand(new EnableRule()));
});
}
[Fact]
public void Enable_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Enable(CreateRuleCommand(new EnableRule()));
});
}
[Fact]
public void Enable_should_create_events()
{
CreateRule();
var command = new EnableRule();
sut.Enable(CreateRuleCommand(command));
Assert.True(sut.Snapshot.RuleDef.IsEnabled);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleEnabled())
);
}
[Fact]
public void Disable_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Disable(CreateRuleCommand(new DisableRule()));
});
}
[Fact]
public void Disable_should_throw_exception_if_rule_is_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Disable(CreateRuleCommand(new DisableRule()));
});
}
[Fact]
public void Disable_should_create_events()
{
CreateRule();
var command = new DisableRule();
sut.Disable(CreateRuleCommand(command));
Assert.False(sut.Snapshot.RuleDef.IsEnabled);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDisabled())
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateRule();
DeleteRule();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
});
}
[Fact]
public void Delete_should_update_create_events()
{
CreateRule();
sut.Delete(CreateRuleCommand(new DeleteRule()));
Assert.True(sut.Snapshot.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDeleted())
);
}
private void CreateRule()
{
sut.Create(CreateRuleCommand(new CreateRule { Trigger = ruleTrigger, Action = ruleAction }));
sut.ClearUncommittedEvents();
}
private void DeleteRule()
{
sut.Delete(CreateRuleCommand(new DeleteRule()));
sut.ClearUncommittedEvents();
}
protected T CreateRuleEvent<T>(T @event) where T : RuleEvent
{
@event.RuleId = ruleId;
return CreateEvent(@event);
}
protected T CreateRuleCommand<T>(T command) where T : RuleCommand
{
command.RuleId = ruleId;
return CreateCommand(command);
}
}
}

214
tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

@ -0,0 +1,214 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleGrainTests : HandlerTestBase<RuleGrain, RuleState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly Guid ruleId = Guid.NewGuid();
private readonly RuleGrain sut;
protected override Guid Id
{
get { return ruleId; }
}
public RuleGrainTests()
{
sut = new RuleGrain(Store, appProvider);
sut.ActivateAsync(Id).Wait();
}
[Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
await Assert.ThrowsAsync<DomainException>(ExecuteDisableAsync);
}
[Fact]
public async Task Create_should_create_events_and_update_state()
{
var command = MakeCreateCommand();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0));
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger);
Assert.Same(command.Action, sut.Snapshot.RuleDef.Action);
LastEvents
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleCreated { Trigger = command.Trigger, Action = command.Action })
);
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var command = MakeUpdateCommand();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Same(command.Trigger, sut.Snapshot.RuleDef.Trigger);
Assert.Same(command.Action, sut.Snapshot.RuleDef.Action);
LastEvents
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleUpdated { Trigger = command.Trigger, Action = command.Action })
);
}
[Fact]
public async Task Enable_should_handle_command()
{
await sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand()));
await sut.ExecuteAsync(CreateRuleCommand(new DisableRule()));
}
[Fact]
public async Task Enable_should_create_events_and_update_state()
{
var command = new EnableRule();
await ExecuteCreateAsync();
await ExecuteDisableAsync();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.True(sut.Snapshot.RuleDef.IsEnabled);
LastEvents
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleEnabled())
);
}
[Fact]
public async Task Disable_should_create_events_and_update_state()
{
var command = new DisableRule();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.False(sut.Snapshot.RuleDef.IsEnabled);
LastEvents
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDisabled())
);
}
[Fact]
public async Task Delete_should_update_create_events()
{
var command = new DeleteRule();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateRuleCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted);
LastEvents
.ShouldHaveSameEvents(
CreateRuleEvent(new RuleDeleted())
);
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateRuleCommand(MakeCreateCommand()));
}
private Task ExecuteDisableAsync()
{
return sut.ExecuteAsync(CreateRuleCommand(new DisableRule()));
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateRuleCommand(new DeleteRule()));
}
protected T CreateRuleEvent<T>(T @event) where T : RuleEvent
{
@event.RuleId = ruleId;
return CreateEvent(@event);
}
protected T CreateRuleCommand<T>(T command) where T : RuleCommand
{
command.RuleId = ruleId;
return CreateCommand(command);
}
private CreateRule MakeCreateCommand()
{
var newTrigger = new ContentChangedTrigger
{
Schemas = ImmutableList<ContentChangedTriggerSchema>.Empty
};
var newAction = new WebhookAction
{
Url = new Uri("https://squidex.io/v2")
};
return new CreateRule { Trigger = newTrigger, Action = newAction };
}
private static UpdateRule MakeUpdateCommand()
{
var newTrigger = new ContentChangedTrigger
{
Schemas = ImmutableList<ContentChangedTriggerSchema>.Empty
};
var newAction = new WebhookAction
{
Url = new Uri("https://squidex.io/v2")
};
return new UpdateRule { Trigger = newTrigger, Action = newAction };
}
}
}

280
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaCommandMiddlewareTests.cs

@ -1,280 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaCommandMiddlewareTests : HandlerTestBase<SchemaDomainObject>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly SchemaCommandMiddleware sut;
private readonly SchemaDomainObject schema;
private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry());
private readonly string fieldName = "age";
protected override Guid Id
{
get { return SchemaId; }
}
public SchemaCommandMiddlewareTests()
{
schema = new SchemaDomainObject(registry);
sut = new SchemaCommandMiddleware(Handler, appProvider);
A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName))
.Returns((ISchemaEntity)null);
}
[Fact]
public async Task Create_should_create_schema_domain_object()
{
var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId });
await TestCreate(schema, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(SchemaId, context.Result<EntityCreatedResult<Guid>>().IdOrValue);
A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName)).MustHaveHappened();
}
[Fact]
public async Task UpdateSchema_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new UpdateSchema { Properties = new SchemaProperties() });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task ReorderSchema_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new ReorderFields { FieldIds = new List<long>() });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task PublishSchema_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new PublishSchema());
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task UnpublishSchema_should_update_domain_object()
{
CreateSchema();
PublishSchema();
var context = CreateContextForCommand(new UnpublishSchema());
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task ConfigureScripts_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new ConfigureScripts());
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DeleteSchema_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new DeleteSchema());
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task Add_should_update_domain_object()
{
CreateSchema();
var context = CreateContextForCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
Assert.Equal(1, context.Result<EntityCreatedResult<long>>().IdOrValue);
}
[Fact]
public async Task UpdateField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task LockField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new LockField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task HideField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new HideField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task ShowField_should_update_domain_object()
{
CreateSchema();
CreateField();
HideField();
var context = CreateContextForCommand(new ShowField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DisableField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new DisableField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task EnableField_should_update_domain_object()
{
CreateSchema();
CreateField();
DisableField();
var context = CreateContextForCommand(new EnableField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task DeleteField_should_update_domain_object()
{
CreateSchema();
CreateField();
var context = CreateContextForCommand(new DeleteField { FieldId = 1 });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
private void CreateSchema()
{
schema.Create(CreateCommand(new CreateSchema { Name = SchemaName }));
}
private void PublishSchema()
{
schema.Publish(CreateCommand(new PublishSchema()));
}
private void CreateField()
{
schema.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }));
}
private void HideField()
{
schema.HideField(CreateCommand(new HideField { FieldId = 1 }));
}
private void DisableField()
{
schema.DisableField(CreateCommand(new DisableField { FieldId = 1 }));
}
}
}

665
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs

@ -1,665 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaDomainObjectTests : HandlerTestBase<SchemaDomainObject>
{
private readonly string fieldName = "age";
private readonly NamedId<long> fieldId;
private readonly SchemaDomainObject sut;
protected override Guid Id
{
get { return SchemaId; }
}
public SchemaDomainObjectTests()
{
fieldId = new NamedId<long>(1, fieldName);
var fieldRegistry = new FieldRegistry(new TypeNameRegistry());
sut = new SchemaDomainObject(fieldRegistry);
sut.ActivateAsync(Id, A.Fake<IStore<Guid>>());
}
[Fact]
public void Create_should_throw_exception_if_created()
{
sut.Create(CreateCommand(new CreateSchema { Name = SchemaName }));
Assert.Throws<DomainException>(() =>
{
sut.Create(CreateCommand(new CreateSchema { Name = SchemaName }));
});
}
[Fact]
public void Create_should_create_schema_and_create_events()
{
var properties = new SchemaProperties();
sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties }));
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Equal(SchemaName, sut.Snapshot.Name);
Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties })
);
}
[Fact]
public void Create_should_create_schema_with_initial_fields()
{
var properties = new SchemaProperties();
var fields = new List<CreateSchemaField>
{
new CreateSchemaField { Name = "field1", Properties = ValidProperties() },
new CreateSchemaField { Name = "field2", Properties = ValidProperties() }
};
sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields }));
var @event = (SchemaCreated)sut.GetUncomittedEvents().Single().Payload;
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Equal(SchemaName, sut.Snapshot.Name);
Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name);
Assert.Equal(2, @event.Fields.Count);
}
[Fact]
public void Update_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() }));
});
}
[Fact]
public void Update_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() }));
});
}
[Fact]
public void Update_should_refresh_properties_and_create_events()
{
var properties = new SchemaProperties();
CreateSchema();
sut.Update(CreateCommand(new UpdateSchema { Properties = properties }));
Assert.Equal(properties, sut.Snapshot.SchemaDef.Properties);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaUpdated { Properties = properties })
);
}
[Fact]
public void ConfigureScripts_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.ConfigureScripts(CreateCommand(new ConfigureScripts()));
});
}
[Fact]
public void ConfigureScripts_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.ConfigureScripts(CreateCommand(new ConfigureScripts()));
});
}
[Fact]
public void ConfigureScripts_should_create_events()
{
CreateSchema();
sut.ConfigureScripts(CreateCommand(new ConfigureScripts
{
ScriptQuery = "<script-query>",
ScriptCreate = "<script-create>",
ScriptUpdate = "<script-update>",
ScriptDelete = "<script-delete>",
ScriptChange = "<script-change>"
}));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new ScriptsConfigured
{
ScriptQuery = "<script-query>",
ScriptCreate = "<script-create>",
ScriptUpdate = "<script-update>",
ScriptDelete = "<script-delete>",
ScriptChange = "<script-change>"
})
);
}
[Fact]
public void Reorder_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List<long>() }));
});
}
[Fact]
public void Reorder_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List<long>() }));
});
}
[Fact]
public void Reorder_should_refresh_properties_and_create_events()
{
var fieldIds = new List<long> { 1, 2 };
CreateSchema();
sut.Add(CreateCommand(new AddField { Name = "field1", Properties = ValidProperties() }));
sut.Add(CreateCommand(new AddField { Name = "field2", Properties = ValidProperties() }));
sut.ClearUncommittedEvents();
sut.Reorder(CreateCommand(new ReorderFields { FieldIds = fieldIds }));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaFieldsReordered { FieldIds = fieldIds })
);
}
[Fact]
public void Publish_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Publish(CreateCommand(new PublishSchema()));
});
}
[Fact]
public void Publish_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Publish(CreateCommand(new PublishSchema()));
});
}
[Fact]
public void Publish_should_refresh_properties_and_create_events()
{
CreateSchema();
sut.Publish(CreateCommand(new PublishSchema()));
Assert.True(sut.Snapshot.SchemaDef.IsPublished);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaPublished())
);
}
[Fact]
public void Unpublish_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateCommand(new UnpublishSchema()));
});
}
[Fact]
public void Unpublish_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Unpublish(CreateCommand(new UnpublishSchema()));
});
}
[Fact]
public void Unpublish_should_refresh_properties_and_create_events()
{
CreateSchema();
PublishSchema();
sut.Unpublish(CreateCommand(new UnpublishSchema()));
Assert.False(sut.Snapshot.SchemaDef.IsPublished);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaUnpublished())
);
}
[Fact]
public void Delete_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateCommand(new DeleteSchema()));
});
}
[Fact]
public void Delete_should_throw_exception_if_already_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Delete(CreateCommand(new DeleteSchema()));
});
}
[Fact]
public void Delete_should_refresh_properties_and_create_events()
{
CreateSchema();
sut.Delete(CreateCommand(new DeleteSchema()));
Assert.True(sut.Snapshot.IsDeleted);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new SchemaDeleted())
);
}
[Fact]
public void AddField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = ValidProperties() }));
});
}
[Fact]
public void AddField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }));
});
}
[Fact]
public void Add_should_update_schema_and_create_events()
{
var properties = new NumberFieldProperties();
CreateSchema();
sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = properties }));
Assert.Equal(properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = properties })
);
}
[Fact]
public void UpdateField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() }));
});
}
[Fact]
public void UpdateField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() }));
});
}
[Fact]
public void UpdateField_should_update_schema_and_create_events()
{
var properties = new NumberFieldProperties();
CreateSchema();
CreateField();
sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = properties }));
Assert.Equal(properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = properties })
);
}
[Fact]
public void LockField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
});
}
[Fact]
public void LockField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
});
}
[Fact]
public void LockField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.LockField(CreateCommand(new LockField { FieldId = 1 }));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldLocked { FieldId = fieldId })
);
}
[Fact]
public void HideField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.HideField(CreateCommand(new HideField { FieldId = 1 }));
});
}
[Fact]
public void HideField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.HideField(CreateCommand(new HideField { FieldId = 1 }));
});
}
[Fact]
public void HideField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.HideField(CreateCommand(new HideField { FieldId = 1 }));
Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldHidden { FieldId = fieldId })
);
}
[Fact]
public void ShowField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.ShowField(CreateCommand(new ShowField { FieldId = 1 }));
});
}
[Fact]
public void ShowField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.ShowField(CreateCommand(new ShowField { FieldId = 1 }));
});
}
[Fact]
public void ShowField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.HideField(CreateCommand(new HideField { FieldId = 1 }));
sut.ShowField(CreateCommand(new ShowField { FieldId = 1 }));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden);
sut.GetUncomittedEvents().Skip(1)
.ShouldHaveSameEvents(
CreateEvent(new FieldShown { FieldId = fieldId })
);
}
[Fact]
public void DisableField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.DisableField(CreateCommand(new DisableField { FieldId = 1 }));
});
}
[Fact]
public void DisableField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.DisableField(CreateCommand(new DisableField { FieldId = 1 }));
});
}
[Fact]
public void DisableField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.DisableField(CreateCommand(new DisableField { FieldId = 1 }));
Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldDisabled { FieldId = fieldId })
);
}
[Fact]
public void EnableField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.EnableField(CreateCommand(new EnableField { FieldId = 1 }));
});
}
[Fact]
public void EnableField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.EnableField(CreateCommand(new EnableField { FieldId = 1 }));
});
}
[Fact]
public void EnableField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.DisableField(CreateCommand(new DisableField { FieldId = 1 }));
sut.EnableField(CreateCommand(new EnableField { FieldId = 1 }));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
sut.GetUncomittedEvents().Skip(1)
.ShouldHaveSameEvents(
CreateEvent(new FieldEnabled { FieldId = fieldId })
);
}
[Fact]
public void DeleteField_should_throw_exception_if_not_created()
{
Assert.Throws<DomainException>(() =>
{
sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 }));
});
}
[Fact]
public void DeleteField_should_throw_exception_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 }));
});
}
[Fact]
public void DeleteField_should_update_schema_and_create_events()
{
CreateSchema();
CreateField();
sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 }));
Assert.False(sut.Snapshot.SchemaDef.FieldsById.ContainsKey(1));
sut.GetUncomittedEvents()
.ShouldHaveSameEvents(
CreateEvent(new FieldDeleted { FieldId = fieldId })
);
}
private void CreateField()
{
sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }));
sut.ClearUncommittedEvents();
}
private void CreateSchema()
{
sut.Create(CreateCommand(new CreateSchema { Name = SchemaName }));
sut.ClearUncommittedEvents();
}
private void PublishSchema()
{
sut.Publish(CreateCommand(new PublishSchema()));
sut.ClearUncommittedEvents();
}
private void DeleteSchema()
{
sut.Delete(CreateCommand(new DeleteSchema()));
sut.ClearUncommittedEvents();
}
private static StringFieldProperties ValidProperties()
{
return new StringFieldProperties { MinLength = 10, MaxLength = 20 };
}
private static StringFieldProperties InvalidProperties()
{
return new StringFieldProperties { MinLength = 20, MaxLength = 10 };
}
}
}

428
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs

@ -0,0 +1,428 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaGrainTests : HandlerTestBase<SchemaGrain, SchemaState>
{
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly FieldRegistry registry = new FieldRegistry(new TypeNameRegistry());
private readonly string fieldName = "age";
private readonly NamedId<long> fieldId;
private readonly SchemaGrain sut;
protected override Guid Id
{
get { return SchemaId; }
}
public SchemaGrainTests()
{
A.CallTo(() => appProvider.GetSchemaAsync(AppId, SchemaName))
.Returns((ISchemaEntity)null);
fieldId = new NamedId<long>(1, fieldName);
sut = new SchemaGrain(Store, appProvider, registry);
sut.ActivateAsync(Id).Wait();
}
[Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
await Assert.ThrowsAsync<DomainException>(ExecutePublishAsync);
}
[Fact]
public async Task Create_should_create_schema_and_create_events()
{
var properties = new SchemaProperties();
var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties };
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0));
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Equal(SchemaName, sut.Snapshot.Name);
Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties })
);
}
[Fact]
public async Task Create_should_create_schema_with_initial_fields()
{
var properties = new SchemaProperties();
var fields = new List<CreateSchemaField>
{
new CreateSchemaField { Name = "field1", Properties = ValidProperties() },
new CreateSchemaField { Name = "field2", Properties = ValidProperties() }
};
var command = new CreateSchema { Name = SchemaName, SchemaId = SchemaId, Properties = properties, Fields = fields };
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(Id, 0));
var @event = (SchemaCreated)LastEvents.Single().Payload;
Assert.Equal(AppId, sut.Snapshot.AppId.Id);
Assert.Equal(SchemaName, sut.Snapshot.Name);
Assert.Equal(SchemaName, sut.Snapshot.SchemaDef.Name);
Assert.Equal(2, @event.Fields.Count);
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var command = new UpdateSchema { Properties = new SchemaProperties() };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.Properties);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaUpdated { Properties = command.Properties })
);
}
[Fact]
public async Task ConfigureScripts_should_create_events()
{
var command = new ConfigureScripts
{
ScriptQuery = "<script-query>",
ScriptCreate = "<script-create>",
ScriptUpdate = "<script-update>",
ScriptDelete = "<script-delete>",
ScriptChange = "<script-change>"
};
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new ScriptsConfigured
{
ScriptQuery = "<script-query>",
ScriptCreate = "<script-create>",
ScriptUpdate = "<script-update>",
ScriptDelete = "<script-delete>",
ScriptChange = "<script-change>"
})
);
}
[Fact]
public async Task Reorder_should_create_events_and_update_state()
{
var command = new ReorderFields { FieldIds = new List<long> { 1, 2 } };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync("field1");
await ExecuteAddFieldAsync("field2");
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(3));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaFieldsReordered { FieldIds = command.FieldIds })
);
}
[Fact]
public async Task Publish_should_create_events_and_update_state()
{
var command = new PublishSchema();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.SchemaDef.IsPublished);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaPublished())
);
}
[Fact]
public async Task Unpublish_should_create_events_and_update_state()
{
var command = new UnpublishSchema();
await ExecuteCreateAsync();
await ExecutePublishAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.False(sut.Snapshot.SchemaDef.IsPublished);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaUnpublished())
);
}
[Fact]
public async Task Delete_should_create_events_and_update_state()
{
var command = new DeleteSchema();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new SchemaDeleted())
);
}
[Fact]
public async Task Add_should_create_events_and_update_state()
{
var command = new AddField { Name = fieldName, Properties = ValidProperties() };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(1, 1));
Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldAdded { Name = fieldName, FieldId = fieldId, Properties = command.Properties })
);
}
[Fact]
public async Task UpdateField_should_create_events_and_update_state()
{
var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.Equal(command.Properties, sut.Snapshot.SchemaDef.FieldsById[1].RawProperties);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldUpdated { FieldId = fieldId, Properties = command.Properties })
);
}
[Fact]
public async Task LockField_should_create_events_and_update_state()
{
var command = new LockField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldLocked { FieldId = fieldId })
);
}
[Fact]
public async Task HideField_should_create_events_and_update_state()
{
var command = new HideField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldHidden { FieldId = fieldId })
);
}
[Fact]
public async Task ShowField_should_create_events_and_update_state()
{
var command = new ShowField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
await ExecuteHideFieldAsync(1);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(3));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsHidden);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldShown { FieldId = fieldId })
);
}
[Fact]
public async Task DisableField_should_create_events_and_update_state()
{
var command = new DisableField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.True(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldDisabled { FieldId = fieldId })
);
}
[Fact]
public async Task EnableField_should_create_events_and_update_state()
{
var command = new EnableField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
await ExecuteDisableFieldAsync(1);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(3));
Assert.False(sut.Snapshot.SchemaDef.FieldsById[1].IsDisabled);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldEnabled { FieldId = fieldId })
);
}
[Fact]
public async Task DeleteField_should_create_events_and_update_state()
{
var command = new DeleteField { FieldId = 1 };
await ExecuteCreateAsync();
await ExecuteAddFieldAsync(fieldName);
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(2));
Assert.False(sut.Snapshot.SchemaDef.FieldsById.ContainsKey(1));
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new FieldDeleted { FieldId = fieldId })
);
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateCommand(new CreateSchema { Name = SchemaName }));
}
private Task ExecuteAddFieldAsync(string name)
{
return sut.ExecuteAsync(CreateCommand(new AddField { Properties = ValidProperties(), Name = name }));
}
private Task ExecuteHideFieldAsync(long id)
{
return sut.ExecuteAsync(CreateCommand(new HideField { FieldId = id }));
}
private Task ExecuteDisableFieldAsync(long id)
{
return sut.ExecuteAsync(CreateCommand(new DisableField { FieldId = id }));
}
private Task ExecutePublishAsync()
{
return sut.ExecuteAsync(CreateCommand(new PublishSchema()));
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateCommand(new DeleteSchema()));
}
private static StringFieldProperties ValidProperties()
{
return new StringFieldProperties { MinLength = 10, MaxLength = 20 };
}
}
}

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

@ -40,5 +40,10 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
{
lhs.Should().BeOfType(rhs.GetType());
}
public static void ShouldBeEquivalent<T>(this T result, T value)
{
result.ShouldBeEquivalentTo(value, o => o.IncludingAllDeclaredProperties());
}
}
}

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

@ -6,68 +6,24 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
#pragma warning disable IDE0019 // Use pattern matching
namespace Squidex.Domain.Apps.Entities.TestHelpers
{
public abstract class HandlerTestBase<T> where T : IDomainObject
public abstract class HandlerTestBase<T, TState> where T : IDomainObjectGrain
{
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 Task<V> CreateSyncedAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IDomainObject
{
return CreateAsync(context, creator);
}
public Task<V> UpdateSyncedAsync<V>(CommandContext context, Func<V, Task> creator) where V : class, IDomainObject
{
return UpdateAsync(context, creator);
}
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();
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<TState> persistence = A.Fake<IPersistence<TState>>();
protected RefToken User { get; } = new RefToken("subject", Guid.NewGuid().ToString());
@ -75,8 +31,6 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
protected Guid SchemaId { get; } = Guid.NewGuid();
protected abstract Guid Id { get; }
protected string AppName { get; } = "my-app";
protected string SchemaName { get; } = "my-schema";
@ -91,44 +45,36 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
get { return new NamedId<Guid>(SchemaId, SchemaName); }
}
protected IAggregateHandler Handler
{
get { return handler; }
}
protected abstract Guid Id { get; }
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : SquidexCommand
public IStore<Guid> Store
{
return new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>());
get { return store; }
}
protected async Task TestCreate(T domainObject, Func<T, Task> action, bool shouldCreate = true)
{
handler.Init(domainObject);
await domainObject.ActivateAsync(Id, A.Fake<IStore<Guid>>());
await action(domainObject);
public IEnumerable<Envelope<IEvent>> LastEvents { get; private set; } = Enumerable.Empty<Envelope<IEvent>>();
if (!handler.IsCreated && shouldCreate)
{
throw new InvalidOperationException("Create not called.");
}
protected HandlerTestBase()
{
A.CallTo(() => store.WithSnapshotsAndEventSourcing(A<Type>.Ignored, Id, A<Func<TState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.Invokes(new Action<IEnumerable<Envelope<IEvent>>>(events =>
{
LastEvents = events;
}));
}
protected async Task TestUpdate(T domainObject, Func<T, Task> action, bool shouldUpdate = true)
protected CommandContext CreateContextForCommand<TCommand>(TCommand command) where TCommand : SquidexCommand
{
handler.Init(domainObject);
await domainObject.ActivateAsync(Id, A.Fake<IStore<Guid>>());
await action(domainObject);
if (!handler.IsUpdated && shouldUpdate)
{
throw new InvalidOperationException("Update not called.");
}
return new CommandContext(CreateCommand(command), A.Dummy<ICommandBus>());
}
protected TCommand CreateCommand<TCommand>(TCommand command) where TCommand : SquidexCommand
{
command.ExpectedVersion = EtagVersion.Any;
if (command.Actor == null)
{
command.Actor = User;
@ -151,21 +97,26 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
{
@event.Actor = User;
var appEvent = @event as AppEvent;
EnrichAppInfo(@event);
EnrichSchemaInfo(@event);
return @event;
}
if (appEvent != null)
private void EnrichAppInfo(IEvent @event)
{
if (@event is AppEvent appEvent)
{
appEvent.AppId = AppNamedId;
}
}
var schemaEvent = @event as SchemaEvent;
if (schemaEvent != null)
private void EnrichSchemaInfo(IEvent @event)
{
if (@event is SchemaEvent schemaEvent)
{
schemaEvent.SchemaId = SchemaNamedId;
}
return @event;
}
}
}

284
tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs

@ -1,284 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class AggregateHandlerTests
{
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IStateFactory stateFactory = A.Fake<IStateFactory>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent());
private readonly Envelope<IEvent> event2 = new Envelope<IEvent>(new MyEvent());
private readonly CommandContext context;
private readonly CommandContext invalidContext = new CommandContext(A.Dummy<ICommand>(), A.Dummy<ICommandBus>());
private readonly Guid domainObjectId = Guid.NewGuid();
private readonly MyCommand command;
private readonly MyDomainObject domainObject = new MyDomainObject();
private readonly AggregateHandler sut;
public AggregateHandlerTests()
{
command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any };
context = new CommandContext(command, A.Dummy<ICommandBus>());
A.CallTo(() => store.WithSnapshotsAndEventSourcing(domainObjectId, A<Func<MyDomainState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);
A.CallTo(() => stateFactory.CreateAsync<MyDomainObject>(domainObjectId))
.Returns(Task.FromResult(domainObject));
A.CallTo(() => stateFactory.GetSingleAsync<MyDomainObject>(domainObjectId))
.Returns(Task.FromResult(domainObject));
sut = new AggregateHandler(stateFactory, serviceProvider);
domainObject.ActivateAsync(domainObjectId, store).Wait();
}
[Fact]
public Task Create_with_task_should_throw_exception_if_not_aggregate_command()
{
return Assert.ThrowsAnyAsync<ArgumentException>(() => sut.CreateAsync<MyDomainObject>(invalidContext, x => TaskHelper.False));
}
[Fact]
public Task Create_synced_with_task_should_throw_exception_if_not_aggregate_command()
{
return Assert.ThrowsAnyAsync<ArgumentException>(() => sut.CreateSyncedAsync<MyDomainObject>(invalidContext, x => TaskHelper.False));
}
[Fact]
public Task Create_with_task_should_should_throw_exception_if_version_is_wrong()
{
command.ExpectedVersion = 2;
return Assert.ThrowsAnyAsync<DomainObjectVersionException>(() => sut.CreateAsync<MyDomainObject>(context, x => TaskHelper.False));
}
[Fact]
public Task Create_synced_with_task_should_should_throw_exception_if_version_is_wrong()
{
command.ExpectedVersion = 2;
return Assert.ThrowsAnyAsync<DomainObjectVersionException>(() => sut.CreateSyncedAsync<MyDomainObject>(context, x => TaskHelper.False));
}
[Fact]
public async Task Create_with_task_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.CreateAsync<MyDomainObject>(context, async x =>
{
x.RaiseEvent(new MyEvent());
await Task.Yield();
passedDomainObject = x;
});
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Create_synced_with_task_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.CreateSyncedAsync<MyDomainObject>(context, async x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
await Task.Yield();
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Create_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.CreateAsync<MyDomainObject>(context, x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Create_synced_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.CreateSyncedAsync<MyDomainObject>(context, x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntityCreatedResult<Guid>>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public Task Update_with_task_should_throw_exception_if_not_aggregate_command()
{
return Assert.ThrowsAnyAsync<ArgumentException>(() => sut.UpdateAsync<MyDomainObject>(invalidContext, x => TaskHelper.False));
}
[Fact]
public Task Update_synced_with_task_should_throw_exception_if_not_aggregate_command()
{
return Assert.ThrowsAnyAsync<ArgumentException>(() => sut.UpdateSyncedAsync<MyDomainObject>(invalidContext, x => TaskHelper.False));
}
[Fact]
public Task Update_with_task_should_should_throw_exception_if_version_is_wrong()
{
command.ExpectedVersion = 2;
return Assert.ThrowsAnyAsync<DomainObjectVersionException>(() => sut.UpdateAsync<MyDomainObject>(context, x => TaskHelper.False));
}
[Fact]
public Task Update_synced_with_task_should_should_throw_exception_if_version_is_wrong()
{
command.ExpectedVersion = 2;
return Assert.ThrowsAnyAsync<DomainObjectVersionException>(() => sut.UpdateSyncedAsync<MyDomainObject>(context, x => TaskHelper.False));
}
[Fact]
public async Task Update_with_task_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.UpdateAsync<MyDomainObject>(context, async x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
await Task.Yield();
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Update_synced_with_task_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.UpdateSyncedAsync<MyDomainObject>(context, async x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
await Task.Yield();
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Update_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.UpdateAsync<MyDomainObject>(context, x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
[Fact]
public async Task Update_synced_should_create_domain_object_and_save()
{
MyDomainObject passedDomainObject = null;
await sut.UpdateSyncedAsync<MyDomainObject>(context, x =>
{
x.RaiseEvent(new MyEvent());
x.RaiseEvent(new MyEvent());
passedDomainObject = x;
});
Assert.Equal(2, domainObject.Snapshot.Version);
Assert.Equal(domainObject, passedDomainObject);
Assert.NotNull(context.Result<EntitySavedResult>());
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.MustHaveHappened();
}
}
}

88
tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs

@ -1,88 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class DomainObjectBaseTests
{
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Guid id = Guid.NewGuid();
private readonly MyDomainObject sut = new MyDomainObject();
public DomainObjectBaseTests()
{
A.CallTo(() => store.WithSnapshotsAndEventSourcing(id, A<Func<MyDomainState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);
}
[Fact]
public void Should_instantiate()
{
Assert.Equal(EtagVersion.Empty, sut.Version);
}
[Fact]
public async Task Should_write_state_and_events_when_saved()
{
await sut.ActivateAsync(id, store);
var event1 = new MyEvent();
var event2 = new MyEvent();
var newState = new MyDomainState();
sut.RaiseEvent(event1);
sut.RaiseEvent(event2);
sut.ApplySnapshot(newState);
await sut.WriteAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(newState))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 2)))
.MustHaveHappened();
Assert.Empty(sut.GetUncomittedEvents());
}
[Fact]
public async Task Should_not_ignore_exception_when_saving()
{
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.Throws(new InvalidOperationException());
await sut.ActivateAsync(id, store);
var event1 = new MyEvent();
var event2 = new MyEvent();
var newState = new MyDomainState();
sut.RaiseEvent(event1);
sut.RaiseEvent(event2);
sut.ApplySnapshot(newState);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.WriteAsync());
A.CallTo(() => persistence.WriteSnapshotAsync(newState))
.MustNotHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 2)))
.MustHaveHappened();
Assert.Empty(sut.GetUncomittedEvents());
}
}
}

245
tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs

@ -0,0 +1,245 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class DomainObjectGrainTests
{
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Guid id = Guid.NewGuid();
private readonly MyGrain sut;
public sealed class MyDomainState : IDomainState
{
public long Version { get; set; }
public int Value { get; set; }
}
public sealed class ValueChanged : IEvent
{
public int Value { get; set; }
}
public sealed class CreateAuto : MyCommand
{
public int Value { get; set; }
}
public sealed class CreateCustom : MyCommand
{
public int Value { get; set; }
}
public sealed class UpdateAuto : MyCommand
{
public int Value { get; set; }
}
public sealed class UpdateCustom : MyCommand
{
public int Value { get; set; }
}
public sealed class MyGrain : DomainObjectGrain<MyDomainState>
{
public MyGrain(IStore<Guid> store)
: base(store)
{
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
switch (command)
{
case CreateAuto createAuto:
return CreateAsync(createAuto, c =>
{
RaiseEvent(new ValueChanged { Value = c.Value });
});
case CreateCustom createCustom:
return CreateReturnAsync(createCustom, c =>
{
RaiseEvent(new ValueChanged { Value = c.Value });
return "CREATED";
});
case UpdateAuto updateAuto:
return UpdateAsync(updateAuto, c =>
{
RaiseEvent(new ValueChanged { Value = c.Value });
});
case UpdateCustom updateCustom:
return UpdateReturnAsync(updateCustom, c =>
{
RaiseEvent(new ValueChanged { Value = c.Value });
return "UPDATED";
});
}
return Task.FromResult<object>(null);
}
public override void ApplyEvent(Envelope<IEvent> @event)
{
if (@event.Payload is ValueChanged valueChanged)
{
ApplySnapshot(new MyDomainState { Value = valueChanged.Value });
}
}
}
public DomainObjectGrainTests()
{
A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyGrain), id, A<Func<MyDomainState, Task>>.Ignored, A<Func<Envelope<IEvent>, Task>>.Ignored))
.Returns(persistence);
sut = new MyGrain(store);
}
[Fact]
public void Should_instantiate()
{
Assert.Equal(EtagVersion.Empty, sut.Version);
}
[Fact]
public async Task Should_write_state_and_events_when_created()
{
await SetupEmptyAsync();
var result = await sut.ExecuteAsync(new CreateAuto { Value = 5 });
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 5)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened();
Assert.True(result is EntityCreatedResult<Guid>);
Assert.Empty(sut.GetUncomittedEvents());
Assert.Equal(5, sut.Snapshot.Value);
}
[Fact]
public async Task Should_write_state_and_events_when_updated()
{
await SetupCreatedAsync();
var result = await sut.ExecuteAsync(new UpdateAuto { Value = 5 });
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.That.Matches(x => x.Value == 5)))
.MustHaveHappened();
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.That.Matches(x => x.Count() == 1)))
.MustHaveHappened();
Assert.True(result is EntitySavedResult);
Assert.Empty(sut.GetUncomittedEvents());
Assert.Equal(5, sut.Snapshot.Value);
}
[Fact]
public async Task Should_throw_exception_when_already_created()
{
await SetupCreatedAsync();
await Assert.ThrowsAsync<DomainException>(() => sut.ExecuteAsync(new CreateAuto()));
}
[Fact]
public async Task Should_throw_exception_when_not_created()
{
await SetupEmptyAsync();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ExecuteAsync(new UpdateAuto()));
}
[Fact]
public async Task Should_return_custom_result_on_create()
{
await SetupEmptyAsync();
var result = await sut.ExecuteAsync(new CreateCustom());
Assert.Equal("CREATED", result);
}
[Fact]
public async Task Should_return_custom_result_on_update()
{
await SetupCreatedAsync();
var result = await sut.ExecuteAsync(new UpdateCustom());
Assert.Equal("UPDATED", result);
}
[Fact]
public async Task Should_throw_exception_when_other_verison_expected()
{
await SetupCreatedAsync();
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }));
}
[Fact]
public async Task Should_reset_state_when_writing_snapshot_for_create_failed()
{
await SetupEmptyAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.Ignored))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new CreateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
Assert.Equal(0, sut.Snapshot.Value);
}
[Fact]
public async Task Should_reset_state_when_writing_snapshot_for_update_failed()
{
await SetupCreatedAsync();
A.CallTo(() => persistence.WriteSnapshotAsync(A<MyDomainState>.Ignored))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.ExecuteAsync(new UpdateAuto()));
Assert.Empty(sut.GetUncomittedEvents());
Assert.Equal(0, sut.Snapshot.Value);
}
private async Task SetupCreatedAsync()
{
await sut.ActivateAsync(id);
await sut.ExecuteAsync(new CreateAuto());
}
private async Task SetupEmptyAsync()
{
await sut.ActivateAsync(id);
}
}
}

64
tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs

@ -0,0 +1,64 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class GrainCommandMiddlewareTests
{
private readonly IStateFactory factory = A.Fake<IStateFactory>();
private readonly MyGrain grain = A.Fake<MyGrain>();
private readonly Guid id = Guid.NewGuid();
private readonly GrainCommandMiddleware<MatchingCommand, MyGrain> sut;
public class MatchingCommand : MyCommand
{
}
public GrainCommandMiddlewareTests()
{
A.CallTo(() => factory.CreateAsync<MyGrain>(id))
.Returns(grain);
sut = new GrainCommandMiddleware<MatchingCommand, MyGrain>(factory);
}
[Fact]
public async Task Should_invoke_grain_when_command_is_correct()
{
var command = new MatchingCommand { AggregateId = id };
var context = new CommandContext(command, A.Fake<ICommandBus>());
A.CallTo(() => grain.ExecuteAsync(command))
.Returns(100);
await sut.HandleAsync(context);
Assert.Equal(100, context.Result<int>());
}
[Fact]
public async Task Should_not_invoke_grain_when_command_is_not_correct()
{
var command = new MyCommand { AggregateId = id };
var context = new CommandContext(command, A.Fake<ICommandBus>());
await sut.HandleAsync(context);
Assert.Null(context.Result<object>());
A.CallTo(() => grain.ExecuteAsync(command))
.MustNotHaveHappened();
}
}
}

86
tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
namespace Squidex.Infrastructure.Commands
{
public class SyncedGrainCommandMiddlewareTests
{
private readonly IStateFactory factory = A.Fake<IStateFactory>();
private readonly MyGrain grain = A.Fake<MyGrain>();
private readonly Guid id = Guid.NewGuid();
private readonly SyncedGrainCommandMiddleware<MatchingCommand, MyGrain> sut;
public class MatchingCommand : MyCommand
{
}
public SyncedGrainCommandMiddlewareTests()
{
A.CallTo(() => factory.GetSingleAsync<MyGrain>(id))
.Returns(grain);
sut = new SyncedGrainCommandMiddleware<MatchingCommand, MyGrain>(factory);
}
[Fact]
public async Task Should_invoke_grain_when_command_is_correct()
{
var command = new MatchingCommand { AggregateId = id };
var context = new CommandContext(command, A.Fake<ICommandBus>());
A.CallTo(() => grain.ExecuteAsync(command))
.Returns(100);
await sut.HandleAsync(context);
Assert.Equal(100, context.Result<int>());
A.CallTo(() => factory.Synchronize<MyGrain, Guid>(id))
.MustHaveHappened();
A.CallTo(() => factory.Remove<MyGrain, Guid>(id))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_remove_grain_from_cache_when_failed()
{
var command = new MatchingCommand { AggregateId = id };
var context = new CommandContext(command, A.Fake<ICommandBus>());
A.CallTo(() => grain.ExecuteAsync(command))
.Throws(new InvalidOperationException());
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.HandleAsync(context));
A.CallTo(() => factory.Synchronize<MyGrain, Guid>(id))
.MustNotHaveHappened();
A.CallTo(() => factory.Remove<MyGrain, Guid>(id))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_invoke_grain_when_command_is_not_correct()
{
var command = new MyCommand { AggregateId = id };
var context = new CommandContext(command, A.Fake<ICommandBus>());
await sut.HandleAsync(context);
Assert.Null(context.Result<object>());
A.CallTo(() => grain.ExecuteAsync(command))
.MustNotHaveHappened();
}
}
}

38
tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs

@ -20,8 +20,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
public sealed class MyEventConsumerGrain : EventConsumerGrain
{
public MyEventConsumerGrain(IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log)
: base(eventStore, eventDataFormatter, log)
public MyEventConsumerGrain(IStore<string> store, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log)
: base(store, eventStore, eventDataFormatter, log)
{
}
@ -53,8 +53,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
consumerName = eventConsumer.GetType().Name;
A.CallTo(() => store.WithSnapshots(consumerName, A<Func<EventConsumerState, Task>>.Ignored))
.Invokes(new Action<string, Func<EventConsumerState, Task>>((key, a) => apply = a))
A.CallTo(() => store.WithSnapshots(typeof(EventConsumerGrain), consumerName, A<Func<EventConsumerState, Task>>.Ignored))
.Invokes(new Action<Type, string, Func<EventConsumerState, Task>>((type, key, a) => apply = a))
.Returns(persistence);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope);
sut = new MyEventConsumerGrain(eventStore, formatter, log);
sut = new MyEventConsumerGrain(store, eventStore, formatter, log);
sutSubscriber = sut;
}
@ -80,7 +80,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{
state = state.Stopped();
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
sut.Dispose();
@ -93,7 +93,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public void Should_subscribe_to_event_store_when_not_found_in_db()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
sut.Dispose();
@ -106,7 +106,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public void Should_subscribe_to_event_store_when_not_stopped_in_db()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
sut.Dispose();
@ -119,7 +119,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public void Should_stop_subscription_when_stopped()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
sut.Stop();
sut.Stop();
@ -138,7 +138,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public void Should_reset_consumer_when_resetting()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
sut.Stop();
sut.Reset();
@ -165,7 +165,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_invoke_and_update_position_when_event_received()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
@ -186,7 +186,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_ignore_old_events()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
A.CallTo(() => formatter.Parse(eventData, true))
@ -210,7 +210,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
@ -228,7 +228,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_stop_if_consumer_failed()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var ex = new InvalidOperationException();
@ -249,7 +249,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var ex = new InvalidOperationException();
@ -267,7 +267,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public void Should_stop_if_resetting_failed()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var ex = new InvalidOperationException();
@ -290,7 +290,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_stop_if_handling_failed()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var ex = new InvalidOperationException();
@ -321,7 +321,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_stop_if_deserialization_failed()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var ex = new InvalidOperationException();
@ -350,7 +350,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact]
public async Task Should_start_after_stop_when_handling_failed()
{
sut.ActivateAsync(consumerName, store).Wait();
sut.ActivateAsync(consumerName).Wait();
sut.Activate(eventConsumer);
var exception = new InvalidOperationException();

186
tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

@ -13,7 +13,6 @@ using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -21,48 +20,7 @@ namespace Squidex.Infrastructure.States
{
public class PersistenceEventSourcingTests
{
private class MyStatefulObject : IStatefulObject<string>
{
private readonly List<IEvent> appliedEvents = new List<IEvent>();
private IPersistence persistence;
public long ExpectedVersion { get; set; } = EtagVersion.Any;
public List<IEvent> AppliedEvents
{
get { return appliedEvents; }
}
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload));
return persistence.ReadAsync(ExpectedVersion);
}
public Task WriteEventsAsync(params IEvent[] events)
{
return persistence.WriteEventsAsync(events.Select(Envelope.Create).ToArray());
}
}
private class MyStatefulObjectWithSnapshot : IStatefulObject<string>
{
private IPersistence<object> persistence;
public long ExpectedVersion { get; set; } = EtagVersion.Any;
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithSnapshotsAndEventSourcing<object>(key, s => TaskHelper.Done, s => TaskHelper.Done);
return persistence.ReadAsync(ExpectedVersion);
}
}
private readonly string key = Guid.NewGuid().ToString();
private readonly MyStatefulObject statefulObject = new MyStatefulObject();
private readonly MyStatefulObjectWithSnapshot statefulObjectWithSnapShot = new MyStatefulObjectWithSnapshot();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
@ -70,42 +28,53 @@ namespace Squidex.Infrastructure.States
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<object, string> snapshotStore = A.Fake<ISnapshotStore<object, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
private readonly IStore<string> sut;
public PersistenceEventSourcingTests()
{
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot)))
.Returns(statefulObjectWithSnapShot);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<object, string>)))
.Returns(snapshotStore);
A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key))
.Returns(key);
A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObjectWithSnapshot), key))
A.CallTo(() => streamNameResolver.GetStreamName(typeof(object), key))
.Returns(key);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
sut.Initialize();
sut = new Store<string>(eventStore, eventDataFormatter, services, streamNameResolver);
}
[Fact]
public async Task Should_read_from_store()
{
statefulObject.ExpectedVersion = 1;
var event1 = new MyEvent();
var event2 = new MyEvent();
SetupEventStore(event1, event2);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
await persistence.ReadAsync();
Assert.Equal(persistedEvents.ToArray(), new[] { event1, event2 });
}
[Fact]
public async Task Should_ignore_old_events()
{
var storedEvent = new StoredEvent("1", 0, new EventData());
A.CallTo(() => eventStore.QueryAsync(key, 0))
.Returns(new List<StoredEvent> { storedEvent });
A.CallTo(() => eventDataFormatter.Parse(storedEvent.Data, true))
.Throws(new TypeNameNotFoundException());
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
await persistence.ReadAsync();
Assert.Equal(actualObject.AppliedEvents, new[] { event1, event2 });
Assert.Empty(persistedEvents);
Assert.Equal(0, persistence.Version);
}
[Fact]
@ -116,7 +85,11 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3, 2);
await sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key);
var persistedState = (object)null;
var persistedEvents = new List<IEvent>();
var persistence = sut.WithSnapshotsAndEventSourcing<object, object, string>(key, x => persistedState = x, x => persistedEvents.Add(x.Payload));
await persistence.ReadAsync();
A.CallTo(() => eventStore.QueryAsync(key, 3))
.MustHaveHappened();
@ -130,7 +103,11 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3, 0, 3);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key));
var persistedState = (object)null;
var persistedEvents = new List<IEvent>();
var persistence = sut.WithSnapshotsAndEventSourcing<object, object, string>(key, x => persistedState = x, x => persistedEvents.Add(x.Payload));
await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync());
}
[Fact]
@ -141,50 +118,60 @@ namespace Squidex.Infrastructure.States
SetupEventStore(3, 4, 3);
await Assert.ThrowsAsync<InvalidOperationException>(() => sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key));
var persistedState = (object)null;
var persistedEvents = new List<IEvent>();
var persistence = sut.WithSnapshotsAndEventSourcing<object, object, string>(key, x => persistedState = x, x => persistedEvents.Add(x.Payload));
await Assert.ThrowsAsync<InvalidOperationException>(() => persistence.ReadAsync());
}
[Fact]
public async Task Should_throw_exception_if_not_found()
{
statefulObject.ExpectedVersion = 0;
SetupEventStore(0);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1));
}
[Fact]
public async Task Should_throw_exception_if_other_version_found()
{
statefulObject.ExpectedVersion = 1;
SetupEventStore(3);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.ReadAsync(1));
}
[Fact]
public async Task Should_throw_exception_if_other_version_found_from_snapshot()
{
statefulObjectWithSnapShot.ExpectedVersion = 1;
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2L));
SetupEventStore(0);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObjectWithSnapshot>(key));
var persistedState = (object)null;
var persistedEvents = new List<IEvent>();
var persistence = sut.WithSnapshotsAndEventSourcing<object, object, string>(key, x => persistedState = x, x => persistedEvents.Add(x.Payload));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.ReadAsync(1));
}
[Fact]
public async Task Should_not_throw_exception_if_nothing_expected()
{
statefulObject.ExpectedVersion = EtagVersion.Any;
SetupEventStore(0);
await sut.GetSingleAsync<MyStatefulObject>(key);
var persistedState = (object)null;
var persistedEvents = new List<IEvent>();
var persistence = sut.WithSnapshotsAndEventSourcing<object, object, string>(key, x => persistedState = x, x => persistedEvents.Add(x.Payload));
await persistence.ReadAsync();
}
[Fact]
@ -192,12 +179,13 @@ namespace Squidex.Infrastructure.States
{
SetupEventStore(3);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
Assert.Same(statefulObject, actualObject);
await persistence.ReadAsync();
await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent());
await statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent());
await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create));
await persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create));
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.MustHaveHappened();
@ -210,49 +198,15 @@ namespace Squidex.Infrastructure.States
{
SetupEventStore(3);
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var persistedEvents = new List<IEvent>();
var persistence = sut.WithEventSourcing<object, string>(key, x => persistedEvents.Add(x.Payload));
await persistence.ReadAsync();
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 2, A<ICollection<EventData>>.That.Matches(x => x.Count == 2)))
.Throws(new WrongEventVersionException(1, 1));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteEventsAsync(new MyEvent(), new MyEvent()));
}
[Fact]
public async Task Should_not_remove_from_cache_when_write_failed()
{
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, A<string>.Ignored, A<long>.Ignored, A<ICollection<EventData>>.Ignored))
.Throws(new InvalidOperationException());
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
await Assert.ThrowsAsync<InvalidOperationException>(() => statefulObject.WriteEventsAsync(new MyEvent()));
Assert.True(cache.TryGetValue(key, out var t));
}
[Fact]
public async Task Should_return_same_instance_for_parallel_requests()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.ReturnsLazily(() => Task.Delay(1).ContinueWith(x => ((object)1, 1L)));
var tasks = new List<Task<MyStatefulObject>>();
for (var i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject>(key)));
}
var retrievedStates = await Task.WhenAll(tasks);
foreach (var retrievedState in retrievedStates)
{
Assert.Same(retrievedStates[0], retrievedState);
}
A.CallTo(() => eventStore.QueryAsync(key, 0))
.MustHaveHappened(Repeated.Exactly.Once);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.WriteEventsAsync(new[] { new MyEvent(), new MyEvent() }.Select(Envelope.Create)));
}
private void SetupEventStore(int count, int eventOffset = 0, int readPosition = 0)

171
tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs

@ -6,7 +6,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
@ -18,45 +17,9 @@ using Xunit;
namespace Squidex.Infrastructure.States
{
public class PersistenceSnapshotTests : IDisposable
public class PersistenceSnapshotTests
{
private class MyStatefulObject : IStatefulObject<string>
{
private IPersistence<int> persistence;
private int state;
public long ExpectedVersion { get; set; } = EtagVersion.Any;
public long Version
{
get { return persistence.Version; }
}
public int State
{
get { return state; }
}
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithSnapshots<int, string>(key, s => state = s);
return persistence.ReadAsync(ExpectedVersion);
}
public void SetState(int value)
{
state = value;
}
public Task WriteStateAsync()
{
return persistence.WriteSnapshotAsync(state);
}
}
private readonly string key = Guid.NewGuid().ToString();
private readonly MyStatefulObject statefulObject = new MyStatefulObject();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
@ -64,99 +27,101 @@ namespace Squidex.Infrastructure.States
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<int, string> snapshotStore = A.Fake<ISnapshotStore<int, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
private readonly IStore<string> sut;
public PersistenceSnapshotTests()
{
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int, string>)))
.Returns(snapshotStore);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
sut.Initialize();
}
public void Dispose()
{
sut.Dispose();
sut = new Store<string>(eventStore, eventDataFormatter, services, streamNameResolver);
}
[Fact]
public async Task Should_read_from_store()
{
statefulObject.ExpectedVersion = 1;
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 1));
.Returns((20, 10));
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
await persistence.ReadAsync();
Assert.Equal(123, statefulObject.State);
Assert.Equal(10, persistence.Version);
Assert.Equal(20, persistedState);
}
[Fact]
public async Task Should_set_to_empty_when_store_returns_not_found()
public async Task Should_return_empty_version_when_version_negative()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, EtagVersion.NotFound));
.Returns((20, -10));
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
await persistence.ReadAsync();
Assert.Equal(-1, statefulObject.Version);
Assert.Equal( 0, statefulObject.State);
Assert.Equal(EtagVersion.Empty, persistence.Version);
}
[Fact]
public async Task Should_throw_exception_if_not_found()
public async Task Should_set_to_empty_when_store_returns_not_found()
{
statefulObject.ExpectedVersion = 0;
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, EtagVersion.Empty));
.Returns((20, EtagVersion.Empty));
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject, string>(key));
await persistence.ReadAsync();
Assert.Equal(-1, persistence.Version);
Assert.Equal( 0, persistedState);
}
[Fact]
public async Task Should_throw_exception_if_other_version_found()
public async Task Should_throw_exception_if_not_found_and_version_expected()
{
statefulObject.ExpectedVersion = 1;
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2));
.Returns((123, EtagVersion.Empty));
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject, string>(key));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => persistence.ReadAsync(1));
}
[Fact]
public async Task Should_not_throw_exception_if_noting_expected()
public async Task Should_throw_exception_if_other_version_found()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, EtagVersion.Empty));
.Returns((123, 2));
await sut.GetSingleAsync<MyStatefulObject, string>(key);
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.ReadAsync(1));
}
[Fact]
public async Task Should_write_to_store_with_previous_version()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 13));
.Returns((20, 10));
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
Assert.Same(statefulObject, actualObject);
Assert.Equal(123, statefulObject.State);
await persistence.ReadAsync();
statefulObject.SetState(456);
Assert.Equal(10, persistence.Version);
Assert.Equal(20, persistedState);
await statefulObject.WriteStateAsync();
await persistence.WriteSnapshotAsync(100);
A.CallTo(() => snapshotStore.WriteAsync(key, 456, 13, 14))
A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11))
.MustHaveHappened();
}
@ -164,51 +129,17 @@ namespace Squidex.Infrastructure.States
public async Task Should_wrap_exception_when_writing_to_store_with_previous_version()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 13));
.Returns((20, 10));
A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14))
A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11))
.Throws(new InconsistentStateException(1, 1, new InvalidOperationException()));
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
var persistedState = 0;
var persistence = sut.WithSnapshots<object, int, string>(key, x => persistedState = x);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteStateAsync());
}
await persistence.ReadAsync();
[Fact]
public async Task Should_not_remove_from_cache_when_write_failed()
{
A.CallTo(() => snapshotStore.WriteAsync(A<string>.Ignored, A<int>.Ignored, A<long>.Ignored, A<long>.Ignored))
.Throws(new InvalidOperationException());
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
await Assert.ThrowsAsync<InvalidOperationException>(() => statefulObject.WriteStateAsync());
Assert.True(cache.TryGetValue(key, out var t));
}
[Fact]
public async Task Should_return_same_instance_for_parallel_requests()
{
A.CallTo(() => snapshotStore.ReadAsync(key))
.ReturnsLazily(() => Task.Delay(1).ContinueWith(x => (1, 1L)));
var tasks = new List<Task<MyStatefulObject>>();
for (var i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject, string>(key)));
}
var retrievedStates = await Task.WhenAll(tasks);
foreach (var retrievedState in retrievedStates)
{
Assert.Same(retrievedStates[0], retrievedState);
}
A.CallTo(() => snapshotStore.ReadAsync(key))
.MustHaveHappened(Repeated.Exactly.Once);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => persistence.WriteSnapshotAsync(100));
}
}
}

11
tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
using Xunit;
@ -22,7 +21,7 @@ namespace Squidex.Infrastructure.States
{
private class MyStatefulObject : IStatefulObject<string>
{
public Task ActivateAsync(string key, IStore<string> store)
public Task ActivateAsync(string key)
{
return TaskHelper.Done;
}
@ -30,23 +29,17 @@ namespace Squidex.Infrastructure.States
private readonly string key = Guid.NewGuid().ToString();
private readonly MyStatefulObject statefulObject = new MyStatefulObject();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<int, string> snapshotStore = A.Fake<ISnapshotStore<int, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
public StateFactoryTests()
{
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int, string>)))
.Returns(snapshotStore);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
sut = new StateFactory(pubSub, cache, services);
sut.Initialize();
}

4
tests/Squidex.Infrastructure.Tests/TestHelpers/MyCommand.cs

@ -11,11 +11,11 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Infrastructure.TestHelpers
{
internal sealed class MyCommand : IAggregateCommand, ITimestampCommand
public class MyCommand : IAggregateCommand, ITimestampCommand
{
public Guid AggregateId { get; set; }
public long ExpectedVersion { get; set; }
public long ExpectedVersion { get; set; } = EtagVersion.Any;
public Instant Timestamp { get; set; }
}

15
tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

@ -1,15 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Infrastructure.TestHelpers
{
internal sealed class MyDomainObject : DomainObjectBase<MyDomainState>
{
}
}

27
tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.TestHelpers
{
public class MyGrain : DomainObjectGrain<MyDomainState>
{
public MyGrain(IStore<Guid> store)
: base(store)
{
}
public override Task<object> ExecuteAsync(IAggregateCommand command)
{
return Task.FromResult<object>(null);
}
}
}

6
tools/Migrate_01/Migrations/AddPatterns.cs

@ -34,7 +34,7 @@ namespace Migrate_01.Migrations
foreach (var id in ids)
{
var app = await stateFactory.GetSingleAsync<AppDomainObject>(id);
var app = await stateFactory.GetSingleAsync<AppGrain>(id);
if (app.Snapshot.Patterns.Count == 0)
{
@ -51,10 +51,8 @@ namespace Migrate_01.Migrations
Message = pattern.Message
};
app.AddPattern(command);
await app.ExecuteAsync(command);
}
await app.WriteAsync();
}
}
}

8
tools/Migrate_01/Rebuilder.cs

@ -79,7 +79,7 @@ namespace Migrate_01
{
if (@event.Payload is AssetEvent assetEvent && handledIds.Add(assetEvent.AssetId))
{
var asset = await stateFactory.CreateAsync<AssetDomainObject>(assetEvent.AssetId);
var asset = await stateFactory.CreateAsync<AssetGrain>(assetEvent.AssetId);
asset.ApplySnapshot(asset.Snapshot.Apply(@event));
@ -107,19 +107,19 @@ namespace Migrate_01
{
if (@event.Payload is SchemaEvent schemaEvent && handledIds.Add(schemaEvent.SchemaId.Id))
{
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaEvent.SchemaId.Id);
var schema = await stateFactory.GetSingleAsync<SchemaGrain>(schemaEvent.SchemaId.Id);
await schema.WriteSnapshotAsync();
}
else if (@event.Payload is RuleEvent ruleEvent && handledIds.Add(ruleEvent.RuleId))
{
var rule = await stateFactory.GetSingleAsync<RuleDomainObject>(ruleEvent.RuleId);
var rule = await stateFactory.GetSingleAsync<RuleGrain>(ruleEvent.RuleId);
await rule.WriteSnapshotAsync();
}
else if (@event.Payload is AppEvent appEvent && handledIds.Add(appEvent.AppId.Id))
{
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appEvent.AppId.Id);
var app = await stateFactory.GetSingleAsync<AppGrain>(appEvent.AppId.Id);
await app.WriteSnapshotAsync();
}

Loading…
Cancel
Save