diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 1b76e8aa4..57805b577 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -48,7 +48,7 @@ namespace Squidex.Core.Schemas protected Field(long id, string name) { - Guard.ValidSlug(name, nameof(name)); + Guard.ValidPropertyName(name, nameof(name)); Guard.GreaterThan(id, 0, nameof(id)); this.id = id; diff --git a/src/Squidex.Events/Contents/ContentCreated.cs b/src/Squidex.Events/Contents/ContentCreated.cs new file mode 100644 index 000000000..42c5bb43a --- /dev/null +++ b/src/Squidex.Events/Contents/ContentCreated.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// ContentCreated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Events.Contents +{ + [TypeName("ContentCreatedEvent")] + public class ContentCreated : IEvent + { + public Guid SchemaId { get; set; } + + public JObject Data { get; set; } + } +} diff --git a/src/Squidex.Events/Contents/ContentDeleted.cs b/src/Squidex.Events/Contents/ContentDeleted.cs new file mode 100644 index 000000000..e6d79f30d --- /dev/null +++ b/src/Squidex.Events/Contents/ContentDeleted.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// ContentDeleted.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Events.Contents +{ + [TypeName("ContentDeletedEvent")] + public class ContentDeleted : IEvent + { + } +} diff --git a/src/Squidex.Events/Contents/ContentUpdated.cs b/src/Squidex.Events/Contents/ContentUpdated.cs new file mode 100644 index 000000000..ed1cc47b1 --- /dev/null +++ b/src/Squidex.Events/Contents/ContentUpdated.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// ContentUpdated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Events.Contents +{ + [TypeName("ContentUpdatedEvent")] + public class ContentUpdated : IEvent + { + public JObject Data { get; set; } + } +} diff --git a/src/Squidex.Events/EventExtensions.cs b/src/Squidex.Events/EventExtensions.cs index 2c7f3d12f..3ad5973a8 100644 --- a/src/Squidex.Events/EventExtensions.cs +++ b/src/Squidex.Events/EventExtensions.cs @@ -30,5 +30,22 @@ namespace Squidex.Events return envelope; } + + public static bool HasSchemaId(this EnvelopeHeaders headers) + { + return headers.Contains("SchemaId"); + } + + public static Guid SchemaId(this EnvelopeHeaders headers) + { + return headers["SchemaId"].ToGuid(CultureInfo.InvariantCulture); + } + + public static Envelope SetSchemaId(this Envelope envelope, Guid value) where T : class + { + envelope.Headers.Set("SchemaId", value); + + return envelope; + } } } diff --git a/src/Squidex.Infrastructure/Extensions.cs b/src/Squidex.Infrastructure/Extensions.cs index e638490a7..12938a2cd 100644 --- a/src/Squidex.Infrastructure/Extensions.cs +++ b/src/Squidex.Infrastructure/Extensions.cs @@ -15,12 +15,18 @@ namespace Squidex.Infrastructure public static class Extensions { private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); public static bool IsSlug(this string value) { return value != null && SlugRegex.IsMatch(value); } + public static bool IsPropertyName(this string value) + { + return value != null && PropertyNameRegex.IsMatch(value); + } + public static bool IsBetween(this TValue value, TValue low, TValue high) where TValue : IComparable { return Comparer.Default.Compare(low, value) <= 0 && Comparer.Default.Compare(high, value) >= 0; diff --git a/src/Squidex.Infrastructure/Guard.cs b/src/Squidex.Infrastructure/Guard.cs index bea0e52e3..6cc109d70 100644 --- a/src/Squidex.Infrastructure/Guard.cs +++ b/src/Squidex.Infrastructure/Guard.cs @@ -51,6 +51,18 @@ namespace Squidex.Infrastructure } } + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidPropertyName(string target, string parameterName) + { + NotNullOrEmpty(target, parameterName); + + if (!target.IsPropertyName()) + { + throw new ArgumentException("Target is not a valid property name.", parameterName); + } + } + [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void HasType(object target, string parameterName) diff --git a/src/Squidex.Read/Apps/Repositories/IAppRepository.cs b/src/Squidex.Read/Apps/Repositories/IAppRepository.cs index f087b02f2..2b473cd8b 100644 --- a/src/Squidex.Read/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Read/Apps/Repositories/IAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -15,6 +16,8 @@ namespace Squidex.Read.Apps.Repositories { Task> QueryAllAsync(string subjectId); - Task FindAppByNameAsync(string name); + Task FindAppAsync(Guid appId); + + Task FindAppAsync(string name); } } diff --git a/src/Squidex.Read/Apps/Services/IAppProvider.cs b/src/Squidex.Read/Apps/Services/IAppProvider.cs index c1d668407..0a1e78e25 100644 --- a/src/Squidex.Read/Apps/Services/IAppProvider.cs +++ b/src/Squidex.Read/Apps/Services/IAppProvider.cs @@ -6,12 +6,15 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; namespace Squidex.Read.Apps.Services { public interface IAppProvider { + Task FindAppByIdAsync(Guid id); + Task FindAppByNameAsync(string name); } } diff --git a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs index c4468d8c9..c30718dde 100644 --- a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -23,39 +23,63 @@ namespace Squidex.Read.Apps.Services.Implementations public class CachingAppProvider : CachingProvider, IAppProvider, ICatchEventConsumer { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); - private readonly IAppRepository appRepository; + private readonly IAppRepository repository; private sealed class CacheItem { public IAppEntity Entity; + + public string Name; } - public CachingAppProvider(IMemoryCache cache, IAppRepository appRepository) + public CachingAppProvider(IMemoryCache cache, IAppRepository repository) : base(cache) { Guard.NotNull(cache, nameof(cache)); - this.appRepository = appRepository; + this.repository = repository; + } + + public async Task FindAppByIdAsync(Guid appId) + { + var cacheKey = BuildIdCacheKey(appId); + var cacheItem = Cache.Get(cacheKey); + + if (cacheItem == null) + { + var entity = await repository.FindAppAsync(appId); + + cacheItem = new CacheItem { Entity = entity, Name = entity?.Name }; + + Cache.Set(cacheKey, cacheItem, CacheDuration); + + if (cacheItem.Entity != null) + { + Cache.Set(BuildIdCacheKey(cacheItem.Entity.Id), cacheItem, CacheDuration); + } + } + + return cacheItem.Entity; } public async Task FindAppByNameAsync(string name) { Guard.NotNullOrEmpty(name, nameof(name)); - var cacheKey = BuildModelCacheKey(name); + var cacheKey = BuildNameCacheKey(name); var cacheItem = Cache.Get(cacheKey); if (cacheItem == null) { - var app = await appRepository.FindAppByNameAsync(name); + var entity = await repository.FindAppAsync(name); - cacheItem = new CacheItem { Entity = app }; + cacheItem = new CacheItem { Entity = entity, Name = name }; - Cache.Set(cacheKey, cacheItem, new MemoryCacheEntryOptions { SlidingExpiration = CacheDuration }); + Cache.Set(cacheKey, cacheItem, CacheDuration); if (cacheItem.Entity != null) { - Cache.Set(BuildNamesCacheKey(cacheItem.Entity.Id), cacheItem.Entity.Name, CacheDuration); + Cache.Set(BuildIdCacheKey(cacheItem.Entity.Id), cacheItem, CacheDuration); } } @@ -73,25 +97,29 @@ namespace Squidex.Read.Apps.Services.Implementations @event.Payload is AppLanguageRemoved || @event.Payload is AppMasterLanguageSet) { - var appName = Cache.Get(BuildNamesCacheKey(@event.Headers.AggregateId())); + var cacheKey = BuildIdCacheKey(@event.Headers.AggregateId()); + + var cacheItem = Cache.Get(cacheKey); - if (appName != null) + if (cacheItem?.Name != null) { - Cache.Remove(BuildModelCacheKey(appName)); + Cache.Remove(BuildNameCacheKey(cacheItem.Name)); } + + Cache.Remove(cacheKey); } return Task.FromResult(true); } - private static string BuildNamesCacheKey(Guid schemaId) + private static string BuildNameCacheKey(string name) { - return $"App_Names_{schemaId}"; + return $"App_Ids_{name}"; } - private static string BuildModelCacheKey(string name) + private static string BuildIdCacheKey(Guid schemaId) { - return $"App_{name}"; + return $"App_Names_{schemaId}"; } } } diff --git a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs index e56ec79fe..dc7e8b203 100644 --- a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs @@ -104,7 +104,7 @@ namespace Squidex.Read.Schemas private async Task FindSchemaNameAsync(EnvelopeHeaders headers) { - var schema = await schemaProvider.ProviderSchemaByIdAsync(headers.AggregateId()); + var schema = await schemaProvider.FindSchemaByIdAsync(headers.AggregateId()); return schema.Label ?? schema.Name; } diff --git a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs index 6d3985141..44e10d732 100644 --- a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs @@ -14,8 +14,8 @@ namespace Squidex.Read.Schemas.Services { public interface ISchemaProvider { - Task ProviderSchemaByIdAsync(Guid schemaId); + Task FindSchemaByIdAsync(Guid schemaId); - Task ProvideSchemaByNameAsync(Guid appId, string name); + Task FindSchemaByNameAsync(Guid appId, string name); } } diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index 28366592b..3adad15e5 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -41,7 +41,7 @@ namespace Squidex.Read.Schemas.Services.Implementations this.repository = repository; } - public async Task ProviderSchemaByIdAsync(Guid schemaId) + public async Task FindSchemaByIdAsync(Guid schemaId) { var cacheKey = BuildIdCacheKey(schemaId); var cacheItem = Cache.Get(cacheKey); @@ -63,7 +63,7 @@ namespace Squidex.Read.Schemas.Services.Implementations return cacheItem.Entity; } - public async Task ProvideSchemaByNameAsync(Guid appId, string name) + public async Task FindSchemaByNameAsync(Guid appId, string name) { Guard.NotNullOrEmpty(name, nameof(name)); diff --git a/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs index f96501665..f2eecdd0b 100644 --- a/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using MongoDB.Driver; @@ -53,7 +54,15 @@ namespace Squidex.Store.MongoDb.Apps return entities; } - public async Task FindAppByNameAsync(string name) + public async Task FindAppAsync(Guid id) + { + var entity = + await Collection.Find(s => s.Id == id).FirstOrDefaultAsync(); + + return entity; + } + + public async Task FindAppAsync(string name) { var entity = await Collection.Find(s => s.Name == name).FirstOrDefaultAsync(); diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index 6980b0c47..5307ca7ff 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -42,7 +42,7 @@ namespace Squidex.Write.Apps protected async Task On(CreateApp command, CommandContext context) { - if (await appRepository.FindAppByNameAsync(command.Name) != null) + if (await appRepository.FindAppAsync(command.Name) != null) { var error = new ValidationError($"An app with name '{command.Name}' already exists", diff --git a/src/Squidex.Write/Contents/Commands/CreateContent.cs b/src/Squidex.Write/Contents/Commands/CreateContent.cs new file mode 100644 index 000000000..e265b9fdc --- /dev/null +++ b/src/Squidex.Write/Contents/Commands/CreateContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// CreateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Write.Contents.Commands +{ + public class CreateContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Write/Contents/Commands/DeleteContent.cs b/src/Squidex.Write/Contents/Commands/DeleteContent.cs new file mode 100644 index 000000000..a6380e674 --- /dev/null +++ b/src/Squidex.Write/Contents/Commands/DeleteContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// DeleteContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Write.Contents.Commands +{ + public class DeleteContent : SchemaCommand + { + } +} diff --git a/src/Squidex.Write/Contents/Commands/UpdateContent.cs b/src/Squidex.Write/Contents/Commands/UpdateContent.cs new file mode 100644 index 000000000..9992b23e9 --- /dev/null +++ b/src/Squidex.Write/Contents/Commands/UpdateContent.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// UpdateContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Write.Contents.Commands +{ + public class UpdateContent : ContentDataCommand + { + } +} diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs new file mode 100644 index 000000000..622e307e0 --- /dev/null +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// ContentCommandHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Dispatching; +using Squidex.Read.Apps.Services; +using Squidex.Read.Schemas.Services; +using Squidex.Write.Contents.Commands; + +namespace Squidex.Write.Contents +{ + public class ContentCommandHandler : ICommandHandler + { + private readonly IAggregateHandler handler; + private readonly IAppProvider appProvider; + private readonly ISchemaProvider schemaProvider; + + public ContentCommandHandler( + IAggregateHandler handler, + IAppProvider appProvider, + ISchemaProvider schemaProvider) + { + Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(appProvider, nameof(appProvider)); + Guard.NotNull(schemaProvider, nameof(schemaProvider)); + + this.handler = handler; + this.appProvider = appProvider; + this.schemaProvider = schemaProvider; + } + + protected async Task On(CreateContent command, CommandContext context) + { + await ValidateAsync(command, () => "Failed to create content"); + + await handler.CreateAsync(command, s => + { + s.Create(command); + + context.Succeed(command.AggregateId); + }); + } + + protected async Task On(UpdateContent command, CommandContext context) + { + await ValidateAsync(command, () => "Failed to update content"); + + await handler.UpdateAsync(command, s => s.Update(command)); + } + + protected Task On(DeleteContent command, CommandContext context) + { + return handler.UpdateAsync(command, s => s.Delete(command)); + } + + public Task HandleAsync(CommandContext context) + { + return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + } + + private async Task ValidateAsync(ContentDataCommand command, Func message) + { + Guard.Valid(command, nameof(command), message); + + var taskForApp = appProvider.FindAppByIdAsync(command.AppId); + var taskForSchema = schemaProvider.FindSchemaByIdAsync(command.SchemaId); + + await Task.WhenAll(taskForApp, taskForSchema); + + var errors = new List(); + + await taskForSchema.Result.Schema.ValidateAsync(command.Data, errors, new HashSet(taskForApp.Result.Languages)); + + if (errors.Count > 0) + { + throw new ValidationException(message(), errors); + } + } + } +} diff --git a/src/Squidex.Write/Contents/ContentDataCommand.cs b/src/Squidex.Write/Contents/ContentDataCommand.cs new file mode 100644 index 000000000..334cd2bc1 --- /dev/null +++ b/src/Squidex.Write/Contents/ContentDataCommand.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// ContentDataCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Write.Contents +{ + public class ContentDataCommand : SchemaCommand, IValidatable + { + public JObject Data { get; set; } + + public void Validate(IList errors) + { + if (Data == null) + { + errors.Add(new ValidationError("Data cannot be null", nameof(Data))); + } + } + } +} diff --git a/src/Squidex.Write/Contents/ContentDomainObject.cs b/src/Squidex.Write/Contents/ContentDomainObject.cs new file mode 100644 index 000000000..8489ccb2e --- /dev/null +++ b/src/Squidex.Write/Contents/ContentDomainObject.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// ContentDomainObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Reflection; +using Squidex.Write.Contents.Commands; + +namespace Squidex.Write.Contents +{ + public class ContentDomainObject : DomainObject + { + private bool isDeleted; + private bool isCreated; + + public bool IsDeleted + { + get { return isDeleted; } + } + + public ContentDomainObject(Guid id, int version) + : base(id, version) + { + } + + protected void On(ContentCreated @event) + { + isCreated = true; + } + + protected void On(ContentDeleted @event) + { + isDeleted = true; + } + + public ContentDomainObject Create(CreateContent command) + { + Guard.Valid(command, nameof(command), () => "Cannot create content"); + + VerifyNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); + + return this; + } + + public ContentDomainObject Update(UpdateContent command) + { + Guard.Valid(command, nameof(command), () => "Cannot update content"); + + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentUpdated())); + + return this; + } + + public ContentDomainObject Delete(DeleteContent command) + { + Guard.NotNull(command, nameof(command)); + + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); + + return this; + } + + private void VerifyNotCreated() + { + if (isCreated) + { + throw new DomainException("Content has already been created."); + } + } + + private void VerifyCreatedAndNotDeleted() + { + if (isDeleted || !isCreated) + { + throw new DomainException("Content has already been deleted or not created yet."); + } + } + + protected override void DispatchEvent(Envelope @event) + { + this.DispatchAction(@event.Payload); + } + } +} diff --git a/src/Squidex.Write/ISchemaCommand.cs b/src/Squidex.Write/ISchemaCommand.cs new file mode 100644 index 000000000..3fa5c6b18 --- /dev/null +++ b/src/Squidex.Write/ISchemaCommand.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// ISchemaCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Write +{ + public interface ISchemaCommand : IAppCommand + { + Guid SchemaId { get; set; } + } +} diff --git a/src/Squidex.Write/SchemaAggregateCommand.cs b/src/Squidex.Write/SchemaAggregateCommand.cs new file mode 100644 index 000000000..02c0ce2d2 --- /dev/null +++ b/src/Squidex.Write/SchemaAggregateCommand.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// SchemaAggregateCommand.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Write +{ + public abstract class SchemaAggregateCommand : AppCommand, ISchemaCommand + { + Guid ISchemaCommand.SchemaId + { + get + { + return AggregateId; + } + set + { + AggregateId = value; + } + } + } +} diff --git a/src/Squidex.Write/SchemaCommand.cs b/src/Squidex.Write/SchemaCommand.cs new file mode 100644 index 000000000..74f187141 --- /dev/null +++ b/src/Squidex.Write/SchemaCommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Write +{ + public abstract class SchemaCommand : AppCommand, ISchemaCommand + { + public Guid SchemaId { get; set; } + } +} diff --git a/src/Squidex.Write/Schemas/Commands/AddField.cs b/src/Squidex.Write/Schemas/Commands/AddField.cs index 23aaa52f7..d021fae3f 100644 --- a/src/Squidex.Write/Schemas/Commands/AddField.cs +++ b/src/Squidex.Write/Schemas/Commands/AddField.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Write.Schemas.Commands { - public class AddField : AppCommand, IValidatable + public class AddField : SchemaAggregateCommand, IValidatable { public string Name { get; set; } @@ -20,9 +20,9 @@ namespace Squidex.Write.Schemas.Commands public void Validate(IList errors) { - if (!Name.IsSlug()) + if (!Name.IsPropertyName()) { - errors.Add(new ValidationError("DisplayName must be a valid slug", nameof(Name))); + errors.Add(new ValidationError("DisplayName must be a valid property name", nameof(Name))); } if (Properties == null) diff --git a/src/Squidex.Write/Schemas/Commands/DeleteField.cs b/src/Squidex.Write/Schemas/Commands/DeleteField.cs index ab9c01185..117aad9d6 100644 --- a/src/Squidex.Write/Schemas/Commands/DeleteField.cs +++ b/src/Squidex.Write/Schemas/Commands/DeleteField.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class DeleteField : AppCommand + public class DeleteField : SchemaAggregateCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Write/Schemas/Commands/DeleteSchema.cs b/src/Squidex.Write/Schemas/Commands/DeleteSchema.cs index ef6633465..f65e286cf 100644 --- a/src/Squidex.Write/Schemas/Commands/DeleteSchema.cs +++ b/src/Squidex.Write/Schemas/Commands/DeleteSchema.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class DeleteSchema : AppCommand + public class DeleteSchema : SchemaAggregateCommand { } } \ No newline at end of file diff --git a/src/Squidex.Write/Schemas/Commands/DisableField.cs b/src/Squidex.Write/Schemas/Commands/DisableField.cs index 2ce1307c7..22dc5a8c7 100644 --- a/src/Squidex.Write/Schemas/Commands/DisableField.cs +++ b/src/Squidex.Write/Schemas/Commands/DisableField.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class DisableField : AppCommand + public class DisableField : SchemaAggregateCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Write/Schemas/Commands/EnableField.cs b/src/Squidex.Write/Schemas/Commands/EnableField.cs index 354808377..b15eddee5 100644 --- a/src/Squidex.Write/Schemas/Commands/EnableField.cs +++ b/src/Squidex.Write/Schemas/Commands/EnableField.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class EnableField : AppCommand + public class EnableField : SchemaAggregateCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Write/Schemas/Commands/HideField.cs b/src/Squidex.Write/Schemas/Commands/HideField.cs index 930b0b8d8..d0623b780 100644 --- a/src/Squidex.Write/Schemas/Commands/HideField.cs +++ b/src/Squidex.Write/Schemas/Commands/HideField.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class HideField : AppCommand + public class HideField : SchemaAggregateCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Write/Schemas/Commands/PublishSchema.cs b/src/Squidex.Write/Schemas/Commands/PublishSchema.cs index 265be63d1..43642bf51 100644 --- a/src/Squidex.Write/Schemas/Commands/PublishSchema.cs +++ b/src/Squidex.Write/Schemas/Commands/PublishSchema.cs @@ -8,7 +8,7 @@ namespace Squidex.Write.Schemas.Commands { - public class PublishSchema : AppCommand + public class PublishSchema : SchemaAggregateCommand { } } diff --git a/src/Squidex.Write/Schemas/Commands/ShowField.cs b/src/Squidex.Write/Schemas/Commands/ShowField.cs index 5f7e300d4..9fd43cc8a 100644 --- a/src/Squidex.Write/Schemas/Commands/ShowField.cs +++ b/src/Squidex.Write/Schemas/Commands/ShowField.cs @@ -5,9 +5,10 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== + namespace Squidex.Write.Schemas.Commands { - public class ShowField : AppCommand + public class ShowField : SchemaAggregateCommand { public long FieldId { get; set; } } diff --git a/src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs b/src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs index d3d3649e2..8bfdf2b76 100644 --- a/src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs +++ b/src/Squidex.Write/Schemas/Commands/UnpublishSchema.cs @@ -8,7 +8,7 @@ namespace Squidex.Write.Schemas.Commands { - public class UnpublishSchema : AppCommand + public class UnpublishSchema : SchemaAggregateCommand { } } diff --git a/src/Squidex.Write/Schemas/Commands/UpdateField.cs b/src/Squidex.Write/Schemas/Commands/UpdateField.cs index 7b0d0492b..abc7ddcf9 100644 --- a/src/Squidex.Write/Schemas/Commands/UpdateField.cs +++ b/src/Squidex.Write/Schemas/Commands/UpdateField.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Write.Schemas.Commands { - public class UpdateField : AppCommand, IValidatable + public class UpdateField : SchemaAggregateCommand, IValidatable { public long FieldId { get; set; } diff --git a/src/Squidex.Write/Schemas/Commands/UpdateSchema.cs b/src/Squidex.Write/Schemas/Commands/UpdateSchema.cs index c66fc48bf..e391e9ee4 100644 --- a/src/Squidex.Write/Schemas/Commands/UpdateSchema.cs +++ b/src/Squidex.Write/Schemas/Commands/UpdateSchema.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Write.Schemas.Commands { - public class UpdateSchema : AppCommand, IValidatable + public class UpdateSchema : SchemaAggregateCommand, IValidatable { public SchemaProperties Properties { get; set; } diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index f11efe6c0..5ff35a786 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -32,7 +32,7 @@ namespace Squidex.Write.Schemas protected async Task On(CreateSchema command, CommandContext context) { - if (await schemas.ProvideSchemaByNameAsync(command.AppId, command.Name) != null) + if (await schemas.FindSchemaByNameAsync(command.AppId, command.Name) != null) { var error = new ValidationError($"A schema with name '{command.Name}' already exists", "DisplayName", diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index e8849b9ac..ea4b25d00 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -19,7 +19,7 @@ namespace Squidex.Config.Domain { protected override void Load(ContainerBuilder builder) { - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -27,11 +27,11 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaAggregateIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaAggregateIdHandler.cs deleted file mode 100644 index 706b18a8b..000000000 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaAggregateIdHandler.cs +++ /dev/null @@ -1,69 +0,0 @@ -// ========================================================================== -// EnrichWithSchemaAggregateIdHandler.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Read.Schemas.Services; -using Squidex.Write; -using Squidex.Write.Schemas; - -// ReSharper disable InvertIf - -namespace Squidex.Pipeline.CommandHandlers -{ - public sealed class EnrichWithSchemaAggregateIdHandler : ICommandHandler - { - private readonly ISchemaProvider schemaProvider; - private readonly IActionContextAccessor actionContextAccessor; - - public EnrichWithSchemaAggregateIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor) - { - this.schemaProvider = schemaProvider; - - this.actionContextAccessor = actionContextAccessor; - } - - public async Task HandleAsync(CommandContext context) - { - var aggregateCommand = context.Command as IAggregateCommand; - - if (aggregateCommand == null || aggregateCommand.AggregateId != Guid.Empty) - { - return false; - } - - var appCommand = context.Command as IAppCommand; - - if (appCommand == null) - { - return false; - } - - var routeValues = actionContextAccessor.ActionContext.RouteData.Values; - - if (routeValues.ContainsKey("name")) - { - var schemaName = routeValues["name"].ToString(); - - var schema = await schemaProvider.ProvideSchemaByNameAsync(appCommand.AppId, schemaName); - - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaName, typeof(SchemaDomainObject)); - } - - aggregateCommand.AggregateId = schema.Id; - } - - return false; - } - } -} diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs new file mode 100644 index 000000000..66b93bbe1 --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// EnrichWithSchemaIdHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Read.Schemas.Services; +using Squidex.Write; +using Squidex.Write.Schemas; + +// ReSharper disable InvertIf + +namespace Squidex.Pipeline.CommandHandlers +{ + public sealed class EnrichWithSchemaIdHandler : ICommandHandler + { + private readonly ISchemaProvider schemaProvider; + private readonly IActionContextAccessor actionContextAccessor; + + public EnrichWithSchemaIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor) + { + this.schemaProvider = schemaProvider; + + this.actionContextAccessor = actionContextAccessor; + } + + public async Task HandleAsync(CommandContext context) + { + var schemaCommand = context.Command as ISchemaCommand; + + if (schemaCommand != null) + { + var routeValues = actionContextAccessor.ActionContext.RouteData.Values; + + if (routeValues.ContainsKey("name")) + { + var schemaName = routeValues["name"].ToString(); + + var schema = await schemaProvider.FindSchemaByNameAsync(schemaCommand.AppId, schemaName); + + if (schema == null) + { + throw new DomainObjectNotFoundException(schemaName, typeof(SchemaDomainObject)); + } + + schemaCommand.SchemaId = schema.Id; + } + } + + return false; + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/GuardTests.cs b/tests/Squidex.Infrastructure.Tests/GuardTests.cs index fab60be3e..527947af6 100644 --- a/tests/Squidex.Infrastructure.Tests/GuardTests.cs +++ b/tests/Squidex.Infrastructure.Tests/GuardTests.cs @@ -150,9 +150,10 @@ namespace Squidex.Infrastructure [InlineData(" not-a-slug ")] [InlineData("-not-a-slug-")] [InlineData("not$-a-slug")] + [InlineData("not-a-Slug")] public void ValidSlug_should_throw_for_invalid_slugs(string slug) { - Assert.Throws(() => Guard.ValidSlug(slug, "slug")); + Assert.Throws(() => Guard.ValidSlug(slug, "parameter")); } [Theory] @@ -165,6 +166,33 @@ namespace Squidex.Infrastructure Guard.ValidSlug(slug, "parameter"); } + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(" Not a Property ")] + [InlineData(" not--a--property ")] + [InlineData(" not-a-property ")] + [InlineData("-not-a-property-")] + [InlineData("not$-a-property")] + public void ValidPropertyName_should_throw_for_invalid_slugs(string slug) + { + Assert.Throws(() => Guard.ValidPropertyName(slug, "property")); + } + + [Theory] + [InlineData("property")] + [InlineData("property23")] + [InlineData("other-property")] + [InlineData("other-Property")] + [InlineData("otherProperty")] + [InlineData("just-another-property")] + [InlineData("just-Another-Property")] + [InlineData("justAnotherProperty")] + public void ValidPropertyName_should_do_nothing_for_valid_slugs(string property) + { + Guard.ValidPropertyName(property, "parameter"); + } + [Theory] [InlineData(double.PositiveInfinity)] [InlineData(double.NegativeInfinity)] diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index f4694796d..d821dbf7f 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -53,7 +53,7 @@ namespace Squidex.Write.Apps var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId }; var context = new CommandContext(command); - appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(appName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); await TestCreate(app, async _ => { @@ -69,7 +69,7 @@ namespace Squidex.Write.Apps var command = new CreateApp { Name = appName, AggregateId = Id, Actor = subjectId }; var context = new CommandContext(command); - appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(null)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(appName)).Returns(Task.FromResult(null)).Verifiable(); await TestCreate(app, async _ => { diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs new file mode 100644 index 000000000..4003cd6b2 --- /dev/null +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// ContentCommandHandlerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Moq; +using Newtonsoft.Json.Linq; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Read.Apps; +using Squidex.Read.Apps.Services; +using Squidex.Read.Schemas.Repositories; +using Squidex.Read.Schemas.Services; +using Squidex.Write.Contents.Commands; +using Squidex.Write.Utils; +using Xunit; + +// ReSharper disable ConvertToConstant.Local + +namespace Squidex.Write.Contents +{ + public class ContentCommandHandlerTests : HandlerTestBase + { + private readonly ContentCommandHandler sut; + private readonly ContentDomainObject content; + private readonly Mock schemaProvider = new Mock(); + private readonly Mock appProvider = new Mock(); + private readonly Mock schemaEntity = new Mock(); + private readonly Mock appEntity = new Mock(); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid appId = Guid.NewGuid(); + private readonly JObject data = new JObject(new JProperty("field", 1)); + + public ContentCommandHandlerTests() + { + var schema = + Schema.Create("my-schema", new SchemaProperties()) + .AddOrUpdateField(new NumberField(1, "field", new NumberFieldProperties { IsRequired = true })); + + content = new ContentDomainObject(Id, 0); + + sut = new ContentCommandHandler(Handler, appProvider.Object, schemaProvider.Object); + + appEntity.Setup(x => x.Languages).Returns(new[] { Language.GetLanguage("de") }); + appProvider.Setup(x => x.FindAppByIdAsync(appId)).Returns(Task.FromResult(appEntity.Object)); + + schemaEntity.Setup(x => x.Schema).Returns(schema); + schemaProvider.Setup(x => x.FindSchemaByIdAsync(schemaId)).Returns(Task.FromResult(schemaEntity.Object)); + } + + [Fact] + public async Task Create_should_throw_exception_if_data_is_not_valid() + { + var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() }; + var context = new CommandContext(command); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Create_should_create_content() + { + var command = new CreateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = data }; + var context = new CommandContext(command); + + await TestCreate(content, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(Id, context.Result()); + } + + [Fact] + public async Task Update_should_throw_exception_if_data_is_not_valid() + { + CreateContent(); + + var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = new JObject() }; + var context = new CommandContext(command); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + CreateContent(); + + var command = new UpdateContent { AggregateId = Id, AppId = appId, SchemaId = schemaId, Data = data }; + var context = new CommandContext(command); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateContent(); + + var command = new DeleteContent { AggregateId = Id }; + var context = new CommandContext(command); + + await TestUpdate(content, async _ => + { + await sut.HandleAsync(context); + }); + } + + private void CreateContent() + { + content.Create(new CreateContent { Data = data }); + } + } +} diff --git a/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs new file mode 100644 index 000000000..fd2380f22 --- /dev/null +++ b/tests/Squidex.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -0,0 +1,147 @@ +// ========================================================================== +// ContentDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Squidex.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Write.Contents.Commands; +using Xunit; + +// ReSharper disable ConvertToConstant.Local + +namespace Squidex.Write.Contents +{ + [Collection("Content")] + public class ContentDomainObjectTests + { + private readonly Guid appId = Guid.NewGuid(); + private readonly ContentDomainObject sut; + private readonly JObject data = new JObject(); + + public ContentDomainObjectTests() + { + sut = new ContentDomainObject(Guid.NewGuid(), 0); + } + + [Fact] + public void Create_should_throw_if_created() + { + sut.Create(new CreateContent { Data = data }); + + Assert.Throws(() => sut.Create(new CreateContent { Data = data })); + } + + [Fact] + public void Create_should_throw_if_command_is_not_valid() + { + Assert.Throws(() => sut.Create(new CreateContent())); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(new CreateContent { Data = data, AppId = appId }); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new ContentCreated { Data = data } + }); + } + + [Fact] + public void Update_should_throw_if_not_created() + { + Assert.Throws(() => sut.Update(new UpdateContent { Data = data })); + } + + [Fact] + public void Update_should_throw_if_schema_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => sut.Update(new UpdateContent())); + } + + [Fact] + public void Update_should_throw_if_command_is_not_valid() + { + CreateContent(); + + Assert.Throws(() => sut.Update(new UpdateContent())); + } + + [Fact] + public void Update_should_create_events() + { + CreateContent(); + + sut.Update(new UpdateContent { Data = data }); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new ContentUpdated { Data = data } + }); + } + + [Fact] + public void Delete_should_throw_if_not_created() + { + Assert.Throws(() => sut.Delete(new DeleteContent())); + } + + [Fact] + public void Delete_should_throw_if_already_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => sut.Delete(new DeleteContent())); + } + + [Fact] + public void Delete_should_update_properties_create_events() + { + CreateContent(); + + sut.Delete(new DeleteContent()); + + Assert.True(sut.IsDeleted); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new ContentDeleted() + }); + } + + private void CreateContent() + { + sut.Create(new CreateContent { Data = data, AppId = appId }); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteContent() + { + sut.Delete(new DeleteContent()); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + } +} diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index c84fc601d..4b3853919 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -51,7 +51,7 @@ namespace Squidex.Write.Schemas var command = new CreateSchema { Name = schemaName, AppId = appId, AggregateId = Id }; var context = new CommandContext(command); - schemaProvider.Setup(x => x.ProvideSchemaByNameAsync(appId, schemaName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(appId, schemaName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); await TestCreate(schema, async _ => { @@ -67,7 +67,7 @@ namespace Squidex.Write.Schemas var command = new CreateSchema { Name = schemaName, AppId = appId, AggregateId = Id }; var context = new CommandContext(command); - schemaProvider.Setup(x => x.ProvideSchemaByNameAsync(Id, schemaName)).Returns(Task.FromResult(null)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(Id, schemaName)).Returns(Task.FromResult(null)).Verifiable(); await TestCreate(schema, async _ => { diff --git a/tests/Squidex.Write.Tests/x.ClearAsync() b/tests/Squidex.Write.Tests/x.ClearAsync() new file mode 100644 index 000000000..e69de29bb diff --git a/tests/Squidex.Write.Tests/x.GetEventsAsync() b/tests/Squidex.Write.Tests/x.GetEventsAsync() new file mode 100644 index 000000000..e69de29bb