From 940d11c7132066fbde04d3d398e1481638a3b7c7 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 21 Oct 2017 10:19:27 +0200 Subject: [PATCH 01/10] Retry Subscriber extracted --- .../CQRS/Events/GetEventStoreSubscription.cs | 11 +- src/Squidex.Infrastructure/Actors/Actor.cs | 125 ------- .../Actors/SingleThreadedDispatcher.cs | 69 ++++ .../CQRS/Events/Actors/EventConsumerActor.cs | 328 +++++++----------- .../CQRS/Events/IEventSubscriber.cs | 2 +- .../CQRS/Events/RetrySubscription.cs | 104 ++++++ .../Actors/ActorRemoteTests.cs | 21 +- .../Actors/ActorTests.cs | 158 --------- .../Actors/SingleThreadedDispatcherTests.cs | 91 +++++ .../Events/Actors/EventConsumerActorTests.cs | 76 ++-- .../CQRS/Events/RetrySubscriptionTests.cs | 123 +++++++ 11 files changed, 563 insertions(+), 545 deletions(-) delete mode 100644 src/Squidex.Infrastructure/Actors/Actor.cs create mode 100644 src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs create mode 100644 src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs delete mode 100644 tests/Squidex.Infrastructure.Tests/Actors/ActorTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/CQRS/Events/RetrySubscriptionTests.cs diff --git a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs index 53799cf95..8f6ae46d3 100644 --- a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs @@ -20,7 +20,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events { - internal sealed class GetEventStoreSubscription : IEventSubscription + internal sealed class GetEventStoreSubscription : DisposableObjectBase, IEventSubscription { private const string ProjectionName = "by-{0}-{1}"; private static readonly ConcurrentDictionary SubscriptionsCreated = new ConcurrentDictionary(); @@ -57,11 +57,12 @@ namespace Squidex.Infrastructure.CQRS.Events subscription = SubscribeToStream(streamName); } - public Task StopAsync() + protected override void DisposeObject(bool disposing) { - subscription.Stop(); - - return TaskHelper.Done; + if (disposing) + { + subscription.Stop(); + } } private EventStoreCatchUpSubscription SubscribeToStream(string streamName) diff --git a/src/Squidex.Infrastructure/Actors/Actor.cs b/src/Squidex.Infrastructure/Actors/Actor.cs deleted file mode 100644 index abe5e1658..000000000 --- a/src/Squidex.Infrastructure/Actors/Actor.cs +++ /dev/null @@ -1,125 +0,0 @@ -// ========================================================================== -// Actor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Actors -{ - public abstract class Actor : IDisposable - { - private readonly ActionBlock block; - private bool isStopped; - - private sealed class StopMessage - { - } - - private sealed class ErrorMessage - { - public Exception Exception { get; set; } - } - - protected Actor() - { - var options = new ExecutionDataflowBlockOptions - { - MaxMessagesPerTask = -1, - MaxDegreeOfParallelism = 1, - BoundedCapacity = 10 - }; - - block = new ActionBlock(Handle, options); - } - - public void Dispose() - { - StopAndWaitAsync().Wait(); - } - - protected async Task DispatchAsync(object message) - { - Guard.NotNull(message, nameof(message)); - - await block.SendAsync(message); - } - - protected async Task FailAsync(Exception exception) - { - Guard.NotNull(exception, nameof(exception)); - - await block.SendAsync(new ErrorMessage { Exception = exception }); - } - - protected async Task StopAndWaitAsync() - { - await block.SendAsync(new StopMessage()); - await block.Completion; - } - - protected virtual Task OnStop() - { - return TaskHelper.Done; - } - - protected virtual Task OnError(Exception exception) - { - return TaskHelper.Done; - } - - protected virtual Task OnMessage(object message) - { - return TaskHelper.Done; - } - - private async Task Handle(object message) - { - if (isStopped) - { - return; - } - - switch (message) - { - case StopMessage stopMessage: - { - isStopped = true; - - block.Complete(); - - await OnStop(); - - break; - } - - case ErrorMessage errorMessage: - { - await OnError(errorMessage.Exception); - - break; - } - - default: - { - try - { - await OnMessage(message); - } - catch (Exception ex) - { - await OnError(ex); - } - - break; - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs b/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs new file mode 100644 index 000000000..993ae84f1 --- /dev/null +++ b/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// Actor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Actors +{ + public sealed class SingleThreadedDispatcher + { + private readonly ActionBlock> block; + private bool isStopped; + + public SingleThreadedDispatcher(int capacity = 10) + { + var options = new ExecutionDataflowBlockOptions + { + MaxMessagesPerTask = -1, + MaxDegreeOfParallelism = 1, + BoundedCapacity = capacity + }; + + block = new ActionBlock>(Handle, options); + } + + public Task DispatchAsync(Func action) + { + Guard.NotNull(action, nameof(action)); + + return block.SendAsync(action); + } + + public Task DispatchAsync(Action action) + { + Guard.NotNull(action, nameof(action)); + + return block.SendAsync(() => { action(); return TaskHelper.Done; }); + } + + public async Task StopAndWaitAsync() + { + await DispatchAsync(() => + { + isStopped = true; + + block.Complete(); + }); + + await block.Completion; + } + + private Task Handle(Func action) + { + if (isStopped) + { + return TaskHelper.Done; + } + + return action(); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs index 88a769942..90ab63426 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; -using System.Threading.Tasks.Dataflow; using Squidex.Infrastructure.Actors; using Squidex.Infrastructure.CQRS.Events.Actors.Messages; using Squidex.Infrastructure.Log; @@ -16,53 +15,24 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events.Actors { - public sealed class EventConsumerActor : DisposableObjectBase, IEventSubscriber, IActor + public class EventConsumerActor : DisposableObjectBase, IEventSubscriber, IActor { private readonly EventDataFormatter formatter; - private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); private readonly IEventStore eventStore; private readonly IEventConsumerInfoRepository eventConsumerInfoRepository; private readonly ISemanticLog log; - private readonly ActionBlock dispatcher; - private IEventSubscription eventSubscription; + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1); + private IEventSubscription currentSubscription; private IEventConsumer eventConsumer; - private bool isStopped; private bool statusIsRunning = true; private string statusPosition; private string statusError; - private Guid stateId = Guid.NewGuid(); - private sealed class Teardown + private static Func DefaultFactory { + get { return (e, s, t, p) => new RetrySubscription(e, s, t, p); } } - private sealed class Setup - { - public IEventConsumer EventConsumer { get; set; } - } - - private abstract class SubscriptionMessage - { - public IEventSubscription Subscription { get; set; } - } - - private sealed class SubscriptionEventReceived : SubscriptionMessage - { - public StoredEvent Event { get; set; } - } - - private sealed class SubscriptionFailed : SubscriptionMessage - { - public Exception Exception { get; set; } - } - - private sealed class Reconnect - { - public Guid StateId { get; set; } - } - - public int ReconnectWaitMs { get; set; } = 5000; - public EventConsumerActor( EventDataFormatter formatter, IEventStore eventStore, @@ -79,199 +49,172 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors this.formatter = formatter; this.eventStore = eventStore; this.eventConsumerInfoRepository = eventConsumerInfoRepository; - - var options = new ExecutionDataflowBlockOptions - { - MaxMessagesPerTask = -1, - MaxDegreeOfParallelism = 1, - BoundedCapacity = 10 - }; - - dispatcher = new ActionBlock(OnMessage, options); } protected override void DisposeObject(bool disposing) { if (disposing) { - dispatcher.SendAsync(new Teardown()).Wait(); - dispatcher.Complete(); - dispatcher.Completion.Wait(); + dispatcher.StopAndWaitAsync().Wait(); } } - public async Task WaitForCompletionAsync() + protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) { - while (dispatcher.InputCount > 0) - { - await Task.Delay(20); - } + return new RetrySubscription(eventStore, this, streamFilter, position); } public Task SubscribeAsync(IEventConsumer eventConsumer) { Guard.NotNull(eventConsumer, nameof(eventConsumer)); - return dispatcher.SendAsync(new Setup { EventConsumer = eventConsumer }); + return dispatcher.DispatchAsync(() => HandleSetupAsync(eventConsumer)); } - Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent @event) + private async Task HandleSetupAsync(IEventConsumer consumer) { - return dispatcher.SendAsync(new SubscriptionEventReceived { Subscription = subscription, Event = @event }); - } + eventConsumer = consumer; - Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) - { - return dispatcher.SendAsync(new SubscriptionFailed { Subscription = subscription, Exception = exception }); - } + var status = await eventConsumerInfoRepository.FindAsync(eventConsumer.Name); - void IActor.Tell(object message) - { - dispatcher.SendAsync(message).Forget(); + if (status != null) + { + statusError = status.Error; + statusPosition = status.Position; + statusIsRunning = !status.IsStopped; + } + + if (statusIsRunning) + { + Subscribe(statusPosition); + } } - private async Task OnMessage(object message) + private Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { - if (isStopped) + if (subscription != currentSubscription) { - return; + return TaskHelper.Done; } - try + return DoAndUpdateStateAsync(async () => { - var oldStateId = stateId; - var newStateId = stateId = Guid.NewGuid(); - - switch (message) - { - case Teardown teardown: - { - isStopped = true; - - return; - } - - case Setup setup: - { - eventConsumer = setup.EventConsumer; - - var status = await eventConsumerInfoRepository.FindAsync(eventConsumer.Name); - - if (status != null) - { - statusError = status.Error; - statusPosition = status.Position; - statusIsRunning = !status.IsStopped; - } - - if (statusIsRunning) - { - await SubscribeThisAsync(statusPosition); - } - - break; - } + await DispatchConsumerAsync(formatter.Parse(storedEvent.Data)); - case StartConsumerMessage startConsumer: - { - if (statusIsRunning) - { - return; - } - - await SubscribeThisAsync(statusPosition); + statusError = null; + statusPosition = storedEvent.EventPosition; + }); + } - statusError = null; - statusIsRunning = true; + private Task HandleErrorAsync(IEventSubscription subscription, Exception exception) + { + if (subscription != currentSubscription) + { + return TaskHelper.Done; + } - break; - } + return DoAndUpdateStateAsync(() => + { + Unsubscribe(); - case StopConsumerMessage stopConsumer: - { - if (!statusIsRunning) - { - return; - } + statusError = exception.ToString(); + statusIsRunning = false; + }); + } - await UnsubscribeThisAsync(); + private Task HandleStartAsync() + { + if (statusIsRunning) + { + return TaskHelper.Done; + } - statusIsRunning = false; + return DoAndUpdateStateAsync(() => + { + Subscribe(statusPosition); - break; - } + statusError = null; + statusIsRunning = true; + }); + } - case ResetConsumerMessage resetConsumer: - { - await UnsubscribeThisAsync(); - await ClearAsync(); - await SubscribeThisAsync(null); + private Task HandleStopAsync() + { + if (!statusIsRunning) + { + return TaskHelper.Done; + } - statusError = null; - statusPosition = null; - statusIsRunning = true; + return DoAndUpdateStateAsync(() => + { + Unsubscribe(); - break; - } + statusError = null; + statusIsRunning = false; + }); + } - case Reconnect reconnect: - { - if (!statusIsRunning || reconnect.StateId != oldStateId) - { - return; - } + private Task HandleResetInternalAsync() + { + return DoAndUpdateStateAsync(async () => + { + Unsubscribe(); - await SubscribeThisAsync(statusPosition); + await ClearAsync(); - break; - } + Subscribe(null); - case SubscriptionFailed subscriptionFailed: - { - if (subscriptionFailed.Subscription != eventSubscription) - { - return; - } + statusError = null; + statusPosition = null; + statusIsRunning = true; + }); + } - await UnsubscribeThisAsync(); + Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); + } - if (retryWindow.CanRetryAfterFailure()) - { - Task.Delay(ReconnectWaitMs).ContinueWith(t => dispatcher.SendAsync(new Reconnect { StateId = newStateId })).Forget(); - } - else - { - throw subscriptionFailed.Exception; - } + Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); + } - break; - } + void IActor.Tell(object message) + { + switch (message) + { + case StopConsumerMessage stop: + dispatcher.DispatchAsync(() => HandleStopAsync()).Forget(); + break; - case SubscriptionEventReceived eventReceived: - { - if (eventReceived.Subscription != eventSubscription) - { - return; - } + case StartConsumerMessage stop: + dispatcher.DispatchAsync(() => HandleStartAsync()).Forget(); + break; - var @event = ParseEvent(eventReceived.Event); - - await DispatchConsumerAsync(@event); - - statusError = null; - statusPosition = @eventReceived.Event.EventPosition; + case ResetConsumerMessage stop: + dispatcher.DispatchAsync(() => HandleResetInternalAsync()).Forget(); + break; + } + } - break; - } - } + private Task DoAndUpdateStateAsync(Action action) + { + return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }); + } + private async Task DoAndUpdateStateAsync(Func action) + { + try + { + await action(); await eventConsumerInfoRepository.SetAsync(eventConsumer.Name, statusPosition, !statusIsRunning, statusError); } catch (Exception ex) { try { - await UnsubscribeThisAsync(); + Unsubscribe(); } catch (Exception unsubscribeException) { @@ -290,27 +233,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors } } - private Task UnsubscribeThisAsync() - { - if (eventSubscription != null) - { - eventSubscription.StopAsync().Forget(); - eventSubscription = null; - } - - return TaskHelper.Done; - } - - private Task SubscribeThisAsync(string position) - { - if (eventSubscription == null) - { - eventSubscription = eventStore.CreateSubscription(this, eventConsumer.EventsFilter, position); - } - - return TaskHelper.Done; - } - private async Task ClearAsync() { var actionId = Guid.NewGuid().ToString(); @@ -356,6 +278,24 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors } } + private void Unsubscribe() + { + if (currentSubscription != null) + { + currentSubscription.StopAsync().Forget(); + currentSubscription = null; + } + } + + private void Subscribe(string position) + { + if (currentSubscription == null) + { + currentSubscription?.StopAsync().Forget(); + currentSubscription = CreateSubscription(eventStore, eventConsumer.EventsFilter, position); + } + } + private Envelope ParseEvent(StoredEvent message) { var @event = formatter.Parse(message.Data); diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs index 39772dd99..6957f83c1 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs @@ -13,7 +13,7 @@ namespace Squidex.Infrastructure.CQRS.Events { public interface IEventSubscriber { - Task OnEventAsync(IEventSubscription subscription, StoredEvent @event); + Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent); Task OnErrorAsync(IEventSubscription subscription, Exception exception); } diff --git a/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs b/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs new file mode 100644 index 000000000..5c232455b --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// RetrySubscription.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.Actors; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public sealed class RetrySubscription : IEventSubscription, IEventSubscriber + { + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(10); + private readonly CancellationTokenSource disposeCts = new CancellationTokenSource(); + private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); + private readonly IEventStore eventStore; + private readonly IEventSubscriber eventSubscriber; + private readonly string streamFilter; + private IEventSubscription currentSubscription; + private string position; + + public int ReconnectWaitMs { get; set; } = 5000; + + public RetrySubscription(IEventStore eventStore, IEventSubscriber eventSubscriber, string streamFilter, string position) + { + Guard.NotNull(eventStore, nameof(eventStore)); + Guard.NotNull(eventSubscriber, nameof(eventSubscriber)); + + this.position = position; + + this.eventStore = eventStore; + this.eventSubscriber = eventSubscriber; + + this.streamFilter = streamFilter; + + Subscribe(); + } + + private void Subscribe() + { + currentSubscription = eventStore.CreateSubscription(this, streamFilter, position); + } + + private void Unsubscribe() + { + currentSubscription?.StopAsync().Forget(); + } + + private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + if (subscription == currentSubscription) + { + await eventSubscriber.OnEventAsync(this, storedEvent); + + position = storedEvent.EventPosition; + } + } + + private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception) + { + if (subscription == currentSubscription) + { + subscription.StopAsync().Forget(); + subscription = null; + + if (retryWindow.CanRetryAfterFailure()) + { + Task.Delay(ReconnectWaitMs, disposeCts.Token).ContinueWith(t => + { + dispatcher.DispatchAsync(() => Subscribe()); + }).Forget(); + } + else + { + await eventSubscriber.OnErrorAsync(this, exception); + } + } + } + + Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent)); + } + + Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception)); + } + + public async Task StopAsync() + { + await dispatcher.DispatchAsync(() => Unsubscribe()); + await dispatcher.StopAndWaitAsync(); + + disposeCts.Cancel(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs b/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs index 005570593..e53df6eac 100644 --- a/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs @@ -22,20 +22,23 @@ namespace Squidex.Infrastructure.Actors public int Counter { get; set; } } - private sealed class MyActor : Actor, IActor + private sealed class MyActor : IActor { + private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); + public List Invokes { get; } = new List(); - public void Tell(object message) + public Task StopAndWaitAsync() { - DispatchAsync(message).Forget(); + return dispatcher.StopAndWaitAsync(); } - protected override Task OnMessage(object message) + public void Tell(object message) { - Invokes.Add(message); - - return TaskHelper.Done; + dispatcher.DispatchAsync(() => + { + Invokes.Add(message); + }).Forget(); } } @@ -55,13 +58,13 @@ namespace Squidex.Infrastructure.Actors } [Fact] - public void Should_handle_messages_sequentially() + public async Task Should_handle_messages_sequentially() { remoteActor.Tell(new SuccessMessage { Counter = 1 }); remoteActor.Tell(new SuccessMessage { Counter = 2 }); remoteActor.Tell(new SuccessMessage { Counter = 3 }); - actor.Dispose(); + await actor.StopAndWaitAsync(); actor.Invokes.ShouldBeEquivalentTo(new List { diff --git a/tests/Squidex.Infrastructure.Tests/Actors/ActorTests.cs b/tests/Squidex.Infrastructure.Tests/Actors/ActorTests.cs deleted file mode 100644 index aa6d27a9c..000000000 --- a/tests/Squidex.Infrastructure.Tests/Actors/ActorTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -// ========================================================================== -// ActorTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using FluentAssertions; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Infrastructure.Actors -{ - public class ActorTests - { - public class SuccessMessage - { - public int Counter { get; set; } - } - - public class FailedMessage - { - } - - private sealed class MyActor : Actor, IActor - { - public List Invokes { get; } = new List(); - - public void Tell(Exception exception) - { - FailAsync(exception).Forget(); - } - - public void Tell(object message) - { - DispatchAsync(message).Forget(); - } - - public Task StopAsync() - { - return StopAndWaitAsync(); - } - - protected override Task OnStop() - { - Invokes.Add(true); - - return TaskHelper.Done; - } - - protected override Task OnError(Exception exception) - { - Invokes.Add(exception); - - return TaskHelper.Done; - } - - protected override Task OnMessage(object message) - { - if (message is FailedMessage) - { - throw new InvalidOperationException(); - } - - Invokes.Add(message); - - return TaskHelper.Done; - } - } - - private readonly MyActor sut = new MyActor(); - - [Fact] - public async Task Should_invoke_with_exception() - { - sut.Tell(new InvalidOperationException()); - - await sut.StopAsync(); - - Assert.True(sut.Invokes[0] is InvalidOperationException); - } - - [Fact] - public async Task Should_handle_messages_sequentially() - { - sut.Tell(new SuccessMessage { Counter = 1 }); - sut.Tell(new SuccessMessage { Counter = 2 }); - sut.Tell(new SuccessMessage { Counter = 3 }); - - await sut.StopAsync(); - - sut.Invokes.ShouldBeEquivalentTo(new List - { - new SuccessMessage { Counter = 1 }, - new SuccessMessage { Counter = 2 }, - new SuccessMessage { Counter = 3 }, - true - }); - } - - [Fact] - public async Task Should_raise_error_event_when_event_handling_failed() - { - sut.Tell(new FailedMessage()); - sut.Tell(new SuccessMessage { Counter = 2 }); - sut.Tell(new SuccessMessage { Counter = 3 }); - - await sut.StopAsync(); - - Assert.True(sut.Invokes[0] is InvalidOperationException); - - sut.Invokes.Skip(1).ShouldBeEquivalentTo(new List - { - new SuccessMessage { Counter = 2 }, - new SuccessMessage { Counter = 3 }, - true - }); - } - - [Fact] - public async Task Should_not_handle_messages_after_stop() - { - sut.Tell(new SuccessMessage { Counter = 1 }); - - await sut.StopAsync(); - - sut.Tell(new SuccessMessage { Counter = 2 }); - sut.Tell(new SuccessMessage { Counter = 3 }); - - sut.Tell(new InvalidOperationException()); - - sut.Invokes.ShouldBeEquivalentTo(new List - { - new SuccessMessage { Counter = 1 }, - true - }); - } - - [Fact] - public void Should_call_stop_on_dispose() - { - sut.Tell(new SuccessMessage { Counter = 1 }); - - sut.Dispose(); - - sut.Invokes.ShouldBeEquivalentTo(new List - { - new SuccessMessage { Counter = 1 }, - true - }); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs b/tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs new file mode 100644 index 000000000..4ec54c725 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// SingleThreadedDispatcherTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Actors +{ + public class SingleThreadedDispatcherTests + { + private readonly SingleThreadedDispatcher sut = new SingleThreadedDispatcher(); + + [Fact] + public async Task Should_handle_messages_sequentially() + { + var source = Enumerable.Range(1, 100); + var target = new List(); + + foreach (var item in source) + { + sut.DispatchAsync(() => target.Add(item)).Forget(); + } + + await sut.StopAndWaitAsync(); + + Assert.Equal(source, target); + } + + /* + [Fact] + public async Task Should_raise_error_event_when_event_handling_failed() + { + sut.Tell(new FailedMessage()); + sut.Tell(new SuccessMessage { Counter = 2 }); + sut.Tell(new SuccessMessage { Counter = 3 }); + + await sut.StopAsync(); + + Assert.True(sut.Invokes[0] is InvalidOperationException); + + sut.Invokes.Skip(1).ShouldBeEquivalentTo(new List + { + new SuccessMessage { Counter = 2 }, + new SuccessMessage { Counter = 3 }, + true + }); + } + + [Fact] + public async Task Should_not_handle_messages_after_stop() + { + sut.Tell(new SuccessMessage { Counter = 1 }); + + await sut.StopAsync(); + + sut.Tell(new SuccessMessage { Counter = 2 }); + sut.Tell(new SuccessMessage { Counter = 3 }); + + sut.Tell(new InvalidOperationException()); + + sut.Invokes.ShouldBeEquivalentTo(new List + { + new SuccessMessage { Counter = 1 }, + true + }); + } + + [Fact] + public void Should_call_stop_on_dispose() + { + sut.Tell(new SuccessMessage { Counter = 1 }); + + sut.Dispose(); + + sut.Invokes.ShouldBeEquivalentTo(new List + { + new SuccessMessage { Counter = 1 }, + true + }); + } + */ + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs index eeaeedc12..43a719e3c 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs @@ -33,6 +33,23 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors public string Position { get; set; } } + public sealed class MyEventConsumerActor : EventConsumerActor + { + public MyEventConsumerActor( + EventDataFormatter formatter, + IEventStore eventStore, + IEventConsumerInfoRepository eventConsumerInfoRepository, + ISemanticLog log) + : base(formatter, eventStore, eventConsumerInfoRepository, log) + { + } + + protected override IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) + { + return eventStore.CreateSubscription(this, streamFilter, position); + } + } + private readonly IEventConsumerInfoRepository eventConsumerInfoRepository = A.Fake(); private readonly IEventConsumer eventConsumer = A.Fake(); private readonly IEventStore eventStore = A.Fake(); @@ -59,14 +76,18 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); - sut = new EventConsumerActor(formatter, eventStore, eventConsumerInfoRepository, log) { ReconnectWaitMs = 0 }; + sut = new MyEventConsumerActor( + formatter, + eventStore, + eventConsumerInfoRepository, + log); sutActor = sut; sutSubscriber = sut; } [Fact] - public async Task Should_not_not_subscribe_to_event_store_when_stopped_in_db() + public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() { consumerInfo.IsStopped = true; @@ -87,9 +108,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, null, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -101,9 +119,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) .MustHaveHappened(Repeated.Exactly.Once); } @@ -118,9 +133,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, null)) .MustHaveHappened(Repeated.Exactly.Once); @@ -137,9 +149,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sutActor.Tell(new ResetConsumerMessage()); sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, null)) .MustHaveHappened(Repeated.Exactly.Once); @@ -169,9 +178,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, @event.EventPosition, false, null)) .MustHaveHappened(Repeated.Exactly.Once); @@ -189,43 +195,10 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, @event.EventPosition, false, null)) - .MustNotHaveHappened(); - A.CallTo(() => eventConsumer.On(envelope)) .MustNotHaveHappened(); } - [Fact] - public async Task Should_reopen_subscription_when_exception_is_retrieved() - { - var ex = new InvalidOperationException(); - - await OnSubscribeAsync(); - await OnErrorAsync(eventSubscription, ex); - - await Task.Delay(200); - - await sut.WaitForCompletionAsync(); - - sut.Dispose(); - - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Times(3)); - - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, ex.ToString())) - .MustNotHaveHappened(); - - A.CallTo(() => eventSubscription.StopAsync()) - .MustHaveHappened(Repeated.Exactly.Once); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) - .MustHaveHappened(Repeated.Exactly.Twice); - } - [Fact] public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() { @@ -236,9 +209,6 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors sut.Dispose(); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, null)) - .MustHaveHappened(Repeated.Exactly.Once); - A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, ex.ToString())) .MustNotHaveHappened(); } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/RetrySubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/RetrySubscriptionTests.cs new file mode 100644 index 000000000..a49a4a723 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/RetrySubscriptionTests.cs @@ -0,0 +1,123 @@ +// ========================================================================== +// EventConsumerActorTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public class RetrySubscriptionTests + { + private readonly IEventStore eventStore = A.Fake(); + private readonly IEventSubscriber eventSubscriber = A.Fake(); + private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly IEventSubscriber sutSubscriber; + private readonly RetrySubscription sut; + private readonly string streamFilter = Guid.NewGuid().ToString(); + + public RetrySubscriptionTests() + { + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)).Returns(eventSubscription); + + sut = new RetrySubscription(eventStore, eventSubscriber, streamFilter, null) { ReconnectWaitMs = 0 }; + + sutSubscriber = sut; + } + + [Fact] + public void Should_subscribe_after_constructor() + { + A.CallTo(() => eventStore.CreateSubscription(sut, streamFilter, null)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reopen_subscription_when_exception_is_retrieved() + { + await OnErrorAsync(eventSubscription, new InvalidOperationException()); + + await Task.Delay(200); + + await sut.StopAsync(); + + A.CallTo(() => eventSubscription.StopAsync()) + .MustHaveHappened(Repeated.Exactly.Twice); + + A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) + .MustHaveHappened(Repeated.Exactly.Twice); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_forward_error_from_inner_subscription_when_failed_often() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(eventSubscription, ex); + await OnErrorAsync(eventSubscription, ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_forward_event_from_inner_subscription() + { + var ev = new StoredEvent("1", 2, new EventData()); + + await OnEventAsync(eventSubscription, ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(sut, ev)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_error_when_exception_is_from_another_subscription() + { + var ex = new InvalidOperationException(); + + await OnErrorAsync(A.Fake(), ex); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnErrorAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_forward_event_when_message_is_from_another_subscription() + { + var ev = new StoredEvent("1", 2, new EventData()); + + await OnEventAsync(A.Fake(), ev); + await sut.StopAsync(); + + A.CallTo(() => eventSubscriber.OnEventAsync(A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + } + + private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + { + return sutSubscriber.OnErrorAsync(subscriber, ex); + } + + private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + { + return sutSubscriber.OnEventAsync(subscriber, ev); + } + } +} \ No newline at end of file From 8c4f98296849e6a16898935c8a1fbb63d28096d7 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 21 Oct 2017 16:37:30 +0200 Subject: [PATCH 02/10] Models simplified --- Squidex.sln | 30 ++ Squidex.sln.DotSettings | 4 + .../Apps/AppClient.cs | 43 +++ .../Apps/AppClientPermission.cs | 17 ++ .../Apps/AppClients.cs | 37 +++ .../Apps/AppContributorPermission.cs | 17 ++ .../Apps/AppContributors.cs | 38 +++ .../Apps/AppPermission.cs | 18 ++ .../Apps/RoleExtension.cs | 30 ++ .../Contents/ContentData.cs | 89 ++++++ .../Contents/ContentFieldData.cs | 54 ++++ .../Contents/IdContentData.cs | 51 ++++ .../Contents/NamedContentData.cs | 45 +++ .../Contents/Status.cs | 17 ++ .../Contents/StatusFlow.cs | 33 ++ .../IFieldPartitionItem.cs | 23 ++ .../IFieldPartitioning.cs | 19 ++ .../InvariantPartitioning.cs | 73 +++++ .../LanguageConfig.cs | 66 ++++ .../LanguagesConfig.cs | 172 +++++++++++ .../Partitioning.cs | 50 ++++ .../Schemas/AssetsField.cs | 28 ++ .../Schemas/AssetsFieldProperties.cs | 25 ++ .../Schemas/BooleanField.cs | 28 ++ .../Schemas/BooleanFieldEditor.cs | 16 + .../Schemas/BooleanFieldProperties.cs | 25 ++ .../Schemas/DateTimeCalculatedDefaultValue.cs | 16 + .../Schemas/DateTimeField.cs | 28 ++ .../Schemas/DateTimeFieldEditor.cs | 16 + .../Schemas/DateTimeFieldProperties.cs | 32 ++ .../Schemas/Field.cs | 95 ++++++ .../Schemas/FieldProperties.cs | 23 ++ .../Schemas/FieldRegistry.cs | 116 ++++++++ .../Schemas/Field{T}.cs | 54 ++++ .../Schemas/GeolocationField.cs | 28 ++ .../Schemas/GeolocationFieldEditor.cs | 15 + .../Schemas/GeolocationFieldProperties.cs | 23 ++ .../Schemas/IFieldPropertiesVisitor.cs | 31 ++ .../Schemas/IFieldVisitor.cs | 31 ++ .../Schemas/Json/JsonFieldModel.cs | 36 +++ .../Schemas/Json/JsonSchemaModel.cs | 99 ++++++ .../Schemas/Json/SchemaConverter.cs | 41 +++ .../Schemas/JsonField.cs | 28 ++ .../Schemas/JsonFieldProperties.cs | 21 ++ .../Schemas/NamedElementPropertiesBase.cs | 19 ++ .../Schemas/NumberField.cs | 28 ++ .../Schemas/NumberFieldEditor.cs | 18 ++ .../Schemas/NumberFieldProperties.cs | 31 ++ .../Schemas/ReferencesField.cs | 28 ++ .../Schemas/ReferencesFieldProperties.cs | 28 ++ .../Schemas/Schema.cs | 120 ++++++++ .../Schemas/SchemaProperties.cs | 14 + .../Schemas/StringField.cs | 28 ++ .../Schemas/StringFieldEditor.cs | 20 ++ .../Schemas/StringFieldProperties.cs | 35 +++ .../Schemas/TagsField.cs | 28 ++ .../Schemas/TagsFieldProperties.cs | 25 ++ .../Squidex.Domain.Apps.Core.Model.csproj | 20 ++ .../Webhooks/WebhookSchema.cs | 25 ++ .../ConvertContent/ContentConverter.cs | 201 +++++++++++++ .../EnrichContent/ContentEnricher.cs | 79 +++++ .../ContentEnrichmentExtensions.cs | 23 ++ .../EnrichContent/DefaultValueFactory.cs | 88 ++++++ .../ContentReferencesExtensions.cs | 75 +++++ .../ExtractReferenceIds/ReferencesCleaner.cs | 58 ++++ .../ReferencesExtractor.cs | 63 ++++ .../GenerateEdmSchema/EdmSchemaExtensions.cs | 61 ++++ .../GenerateEdmSchema/EdmTypeVisitor.cs | 77 +++++ .../ContentSchemaBuilder.cs | 57 ++++ .../JsonSchemaExtensions.cs | 72 +++++ .../GenerateJsonSchema/JsonTypeVisitor.cs | 163 ++++++++++ .../ContentWrapper/ContentDataObject.cs | 132 ++++++++ .../ContentWrapper/ContentDataProperty.cs | 68 +++++ .../ContentWrapper/ContentFieldObject.cs | 137 +++++++++ .../ContentWrapper/ContentFieldProperty.cs | 59 ++++ .../Scripting/ContentWrapper/JsonMapper.cs | 146 +++++++++ .../Scripting/IScriptEngine.cs | 21 ++ .../Scripting/JintScriptEngine.cs | 178 +++++++++++ .../Scripting/JintUser.cs | 50 ++++ .../Scripting/ScriptContext.cs | 27 ++ ...Squidex.Domain.Apps.Core.Operations.csproj | 28 ++ .../ContentValidationExtensions.cs | 43 +++ .../ValidateContent/ContentValidator.cs | 142 +++++++++ .../ValidateContent/FieldExtensions.cs | 58 ++++ .../ValidateContent/JsonValueConverter.cs | 113 +++++++ .../ValidateContent/ValidationContext.cs | 59 ++++ .../Validators/AllowedValuesValidator.cs | 45 +++ .../Validators/AssetsValidator.cs | 30 ++ .../Validators/CollectionItemValidator.cs | 48 +++ .../Validators/CollectionValidator.cs | 54 ++++ .../ValidateContent/Validators/IValidator.cs | 18 ++ .../Validators/PatternValidator.cs | 48 +++ .../Validators/RangeValidator.cs | 53 ++++ .../Validators/ReferencesValidator.cs | 37 +++ .../Validators/RequiredStringValidator.cs | 41 +++ .../Validators/RequiredValidator.cs | 27 ++ .../Validators/StringLengthValidator.cs | 49 +++ .../ValidateContent/ValidatorsFactory.cs | 145 +++++++++ tests/RunCoverage.ps1 | 10 +- .../ContentEnrichmentTests.cs | 84 ------ .../{ => Model}/Apps/RoleExtensionTests.cs | 3 +- .../Model/Contents/ContentDataTests.cs | 208 +++++++++++++ .../{ => Model}/Contents/StatusFlowTests.cs | 3 +- .../{ => Model}/InvariantPartitionTests.cs | 2 +- .../{ => Model}/LanguagesConfigTests.cs | 105 +++---- .../{ => Model}/PartitioningTests.cs | 2 +- .../Model/Schemas/FieldRegistryTests.cs | 72 +++++ .../Model/Schemas/Json/JsonSerializerTests.cs | 55 ++++ .../Model/Schemas/SchemaTests.cs | 217 ++++++++++++++ .../ConvertContent/ContentConversionTests.cs} | 205 ++----------- .../EnrichContent/ContentEnrichmentTests.cs | 202 +++++++++++++ .../ReferenceExtractionTests.cs | 243 +++++++++++++++ .../Operations/GenerateEdmSchema/EdmTests.cs | 39 +++ .../GenerateJsonSchema/JsonSchemaTests.cs | 41 +++ .../Scripting/ContentDataObjectTests.cs | 2 +- .../Scripting/JintScriptEngineTests.cs | 3 +- .../Scripting/JintUserTests.cs | 3 +- .../ValidateContent}/AssetsFieldTests.cs | 91 +----- .../ValidateContent}/BooleanFieldTests.cs | 11 +- .../ContentValidationTests.cs | 47 +-- .../ValidateContent}/DateTimeFieldTests.cs | 11 +- .../ValidateContent}/GeolocationFieldTests.cs | 28 +- .../ValidateContent}/JsonFieldTests.cs | 11 +- .../ValidateContent}/NumberFieldTests.cs | 13 +- .../ValidateContent}/ReferencesFieldTests.cs | 104 +------ .../ValidateContent}/StringFieldTests.cs | 13 +- .../ValidateContent}/TagsFieldTests.cs | 11 +- .../ValidationTestExtensions.cs | 6 +- .../Validators/AllowedValuesValidatorTests.cs | 3 +- .../CollectionItemValidatorTests.cs | 3 +- .../Validators/CollectionValidatorTests.cs | 3 +- .../Validators/PatternValidatorTests.cs | 3 +- .../Validators/RangeValidatorTests.cs | 3 +- .../RequiredStringValidatorTests.cs | 3 +- .../Validators/RequiredValidatorTests.cs | 3 +- .../Validators/StringLengthValidatorTests.cs | 3 +- .../Schemas/DateTimePropertiesTests.cs | 46 --- .../Schemas/FieldPropertiesTests.cs | 83 ------ .../Schemas/FieldRegistryTests.cs | 49 --- .../Schemas/Json/JsonSerializerTests.cs | 80 ----- .../Schemas/SchemaTests.cs | 281 ------------------ .../Squidex.Domain.Apps.Core.Tests.csproj | 3 +- .../TestData.cs | 64 ++++ 143 files changed, 6436 insertions(+), 1160 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Partitioning.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj create mode 100644 src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs rename tests/Squidex.Domain.Apps.Core.Tests/{ => Model}/Apps/RoleExtensionTests.cs (95%) create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs rename tests/Squidex.Domain.Apps.Core.Tests/{ => Model}/Contents/StatusFlowTests.cs (93%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Model}/InvariantPartitionTests.cs (97%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Model}/LanguagesConfigTests.cs (58%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Model}/PartitioningTests.cs (98%) create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs rename tests/Squidex.Domain.Apps.Core.Tests/{Contents/ContentDataTests.cs => Operations/ConvertContent/ContentConversionTests.cs} (66%) create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs rename tests/Squidex.Domain.Apps.Core.Tests/{ => Operations}/Scripting/ContentDataObjectTests.cs (99%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Operations}/Scripting/JintScriptEngineTests.cs (98%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Operations}/Scripting/JintUserTests.cs (96%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/AssetsFieldTests.cs (60%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/BooleanFieldTests.cs (90%) rename tests/Squidex.Domain.Apps.Core.Tests/{ => Operations/ValidateContent}/ContentValidationTests.cs (83%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/DateTimeFieldTests.cs (93%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/GeolocationFieldTests.cs (85%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/JsonFieldTests.cs (86%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/NumberFieldTests.cs (90%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/ReferencesFieldTests.cs (56%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/StringFieldTests.cs (91%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/TagsFieldTests.cs (93%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/ValidationTestExtensions.cs (90%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/AllowedValuesValidatorTests.cs (91%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/CollectionItemValidatorTests.cs (92%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/CollectionValidatorTests.cs (94%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/PatternValidatorTests.cs (93%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/RangeValidatorTests.cs (94%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/RequiredStringValidatorTests.cs (94%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/RequiredValidatorTests.cs (92%) rename tests/Squidex.Domain.Apps.Core.Tests/{Schemas => Operations/ValidateContent}/Validators/StringLengthValidatorTests.cs (95%) delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/TestData.cs diff --git a/Squidex.sln b/Squidex.sln index 1c40c8524..fd8cc9914 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -65,6 +65,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution stylecop.json = stylecop.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Model", "src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj", "{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -299,6 +303,30 @@ Global {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x64.Build.0 = Release|Any CPU {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.ActiveCfg = Release|Any CPU {7931187E-A1E6-4F89-8BC8-20A1E445579F}.Release|x86.Build.0 = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x64.Build.0 = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Debug|x86.Build.0 = Debug|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|Any CPU.Build.0 = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x64.ActiveCfg = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x64.Build.0 = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x86.ActiveCfg = Release|Any CPU + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F}.Release|x86.Build.0 = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x64.Build.0 = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Debug|x86.Build.0 = Debug|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|Any CPU.Build.0 = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.ActiveCfg = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x64.Build.0 = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.ActiveCfg = Release|Any CPU + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -328,6 +356,8 @@ Global {42184546-E3CB-4D4F-9495-43979B9C63B9} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} {EF75E488-1324-4E18-A1BD-D3A05AE67B1F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {7931187E-A1E6-4F89-8BC8-20A1E445579F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} + {F0A83301-50A5-40EA-A1A2-07C7858F5A3F} = {C9809D59-6665-471E-AD87-5AC624C65892} + {6B3F75B6-5888-468E-BA4F-4FC725DAEF31} = {C9809D59-6665-471E-AD87-5AC624C65892} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08} diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 322a6eebd..cf27c2d87 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -13,6 +13,10 @@ True True + + + + <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs new file mode 100644 index 000000000..fe4fa0bc5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// AppClient.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppClient + { + private readonly string secret; + private string name; + private AppClientPermission permission; + + public AppClient(string name, string secret) + { + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNullOrEmpty(secret, nameof(secret)); + + this.name = name; + + this.secret = secret; + } + + public void Update(AppClientPermission newPermission) + { + Guard.Enum(newPermission, nameof(newPermission)); + + permission = newPermission; + } + + public void Rename(string newName) + { + Guard.NotNullOrEmpty(newName, nameof(newName)); + + name = newName; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs new file mode 100644 index 000000000..cbc4cdaf2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClientPermission.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// AppClientPermission.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Apps +{ + public enum AppClientPermission + { + Developer, + Editor, + Reader + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs new file mode 100644 index 000000000..07001417f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// AppClients.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public class AppClients + { + private readonly Dictionary clients = new Dictionary(); + + public IReadOnlyDictionary Clients + { + get { return clients; } + } + + public void Add(string id, string secret) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + clients.Add(id, new AppClient(secret, id)); + } + + public void Revoke(string id) + { + Guard.NotNullOrEmpty(id, nameof(id)); + + clients.Remove(id); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs new file mode 100644 index 000000000..c05d7527a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributorPermission.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// AppContributorPermission.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Apps +{ + public enum AppContributorPermission + { + Owner, + Developer, + Editor + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs new file mode 100644 index 000000000..780ed8076 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// AppContributors.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public class AppContributors + { + private readonly Dictionary contributors = new Dictionary(); + + public IReadOnlyDictionary Contributors + { + get { return contributors; } + } + + public void Assign(string contributorId, AppContributorPermission permission) + { + Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); + Guard.Enum(permission, nameof(permission)); + + contributors[contributorId] = permission; + } + + public void Remove(string contributorId) + { + Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); + + contributors.Remove(contributorId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs new file mode 100644 index 000000000..e9bf142a6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// AppPermission.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Apps +{ + public enum AppPermission + { + Owner, + Developer, + Editor, + Reader + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs new file mode 100644 index 000000000..40245615f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// RoleExtension.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public static class RoleExtension + { + public static AppPermission ToAppPermission(this AppClientPermission clientPermission) + { + Guard.Enum(clientPermission, nameof(clientPermission)); + + return (AppPermission)Enum.Parse(typeof(AppPermission), clientPermission.ToString()); + } + + public static AppPermission ToAppPermission(this AppContributorPermission contributorPermission) + { + Guard.Enum(contributorPermission, nameof(contributorPermission)); + + return (AppPermission)Enum.Parse(typeof(AppPermission), contributorPermission.ToString()); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs new file mode 100644 index 000000000..81f11e599 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// ContentData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public abstract class ContentData : Dictionary, IEquatable> + { + public IEnumerable> ValidValues + { + get { return this.Where(x => x.Value != null); } + } + + protected ContentData(IEqualityComparer comparer) + : base(comparer) + { + } + + protected ContentData(IDictionary copy, IEqualityComparer comparer) + : base(copy, comparer) + { + } + + protected static TResult Merge(TResult source, TResult target) where TResult : ContentData + { + if (ReferenceEquals(target, source)) + { + return source; + } + + foreach (var otherValue in source) + { + var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData()); + + foreach (var value in otherValue.Value) + { + fieldValue[value.Key] = value.Value; + } + } + + return target; + } + + protected static TResult Clean(TResult source, TResult target) where TResult : ContentData + { + foreach (var fieldValue in source.ValidValues) + { + var resultValue = new ContentFieldData(); + + foreach (var partitionValue in fieldValue.Value.Where(x => !x.Value.IsNull())) + { + resultValue[partitionValue.Key] = partitionValue.Value; + } + + if (resultValue.Count > 0) + { + target[fieldValue.Key] = resultValue; + } + } + + return target; + } + + public override bool Equals(object obj) + { + return Equals(obj as ContentData); + } + + public bool Equals(ContentData other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs new file mode 100644 index 000000000..cdf5f8464 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// ContentFieldData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class ContentFieldData : Dictionary, IEquatable + { + private static readonly JTokenEqualityComparer JTokenEqualityComparer = new JTokenEqualityComparer(); + + public ContentFieldData() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public ContentFieldData AddValue(string key, JToken value) + { + Guard.NotNullOrEmpty(key, nameof(key)); + + this[key] = value; + + return this; + } + + public ContentFieldData AddValue(JToken value) + { + return AddValue(InvariantPartitioning.Instance.Master.Key, value); + } + + public override bool Equals(object obj) + { + return Equals(obj as ContentFieldData); + } + + public bool Equals(ContentFieldData other) + { + return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other, EqualityComparer.Default, JTokenEqualityComparer)); + } + + public override int GetHashCode() + { + return this.DictionaryHashCode(EqualityComparer.Default, JTokenEqualityComparer); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs new file mode 100644 index 000000000..524e90f82 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// IdContentData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class IdContentData : ContentData, IEquatable + { + public IdContentData() + : base(EqualityComparer.Default) + { + } + + public IdContentData(IdContentData copy) + : base(copy, EqualityComparer.Default) + { + } + + public IdContentData MergeInto(IdContentData target) + { + return Merge(this, target); + } + + public IdContentData ToCleaned() + { + return Clean(this, new IdContentData()); + } + + public IdContentData AddField(long id, ContentFieldData data) + { + Guard.GreaterThan(id, 0, nameof(id)); + + this[id] = data; + + return this; + } + + public bool Equals(IdContentData other) + { + return base.Equals(other); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs new file mode 100644 index 000000000..71cc639f0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// NamedContentData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class NamedContentData : ContentData, IEquatable + { + public NamedContentData() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public NamedContentData MergeInto(NamedContentData target) + { + return Merge(this, target); + } + + public NamedContentData ToCleaned() + { + return Clean(this, new NamedContentData()); + } + + public NamedContentData AddField(string name, ContentFieldData data) + { + Guard.NotNullOrEmpty(name, nameof(name)); + + this[name] = data; + + return this; + } + + public bool Equals(NamedContentData other) + { + return base.Equals(other); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs new file mode 100644 index 000000000..c2f84034d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// Status.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Contents +{ + public enum Status + { + Draft, + Archived, + Published + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs new file mode 100644 index 000000000..30fbf23e1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// StatusFlow.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Domain.Apps.Core.Contents +{ + public static class StatusFlow + { + private static readonly Dictionary Flow = new Dictionary + { + [Status.Draft] = new[] { Status.Published, Status.Archived }, + [Status.Archived] = new[] { Status.Draft }, + [Status.Published] = new[] { Status.Draft, Status.Archived } + }; + + public static bool Exists(Status status) + { + return Flow.ContainsKey(status); + } + + public static bool CanChange(Status status, Status toStatus) + { + return Flow.TryGetValue(status, out var state) && state.Contains(toStatus); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs new file mode 100644 index 000000000..39a4434d2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// IFieldPartitionItem.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public interface IFieldPartitionItem + { + string Key { get; } + + string Name { get; } + + bool IsOptional { get; } + + IEnumerable Fallback { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs new file mode 100644 index 000000000..ff4f97928 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// IFieldPartitioning.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public interface IFieldPartitioning : IReadOnlyCollection + { + IFieldPartitionItem Master { get; } + + bool TryGetItem(string key, out IFieldPartitionItem item); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs b/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs new file mode 100644 index 000000000..5dd1ad0f8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// InvariantPartitioning.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Squidex.Domain.Apps.Core +{ + public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem + { + public static readonly InvariantPartitioning Instance = new InvariantPartitioning(); + + public int Count + { + get { return 1; } + } + + public IFieldPartitionItem Master + { + get { return this; } + } + + string IFieldPartitionItem.Key + { + get { return "iv"; } + } + + string IFieldPartitionItem.Name + { + get { return "Invariant"; } + } + + bool IFieldPartitionItem.IsOptional + { + get { return false; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return Enumerable.Empty(); } + } + + private InvariantPartitioning() + { + } + + public bool TryGetItem(string key, out IFieldPartitionItem item) + { + var isFound = string.Equals(key, "iv", StringComparison.OrdinalIgnoreCase); + + item = isFound ? this : null; + + return isFound; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + + IEnumerator IEnumerable.GetEnumerator() + { + yield return this; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs new file mode 100644 index 000000000..63fd8904f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// LanguageConfig.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core +{ + public sealed class LanguageConfig : IFieldPartitionItem + { + private readonly Language language; + private readonly Language[] languageFallbacks; + private readonly bool isOptional; + + public bool IsOptional + { + get { return isOptional; } + } + + public Language Language + { + get { return language; } + } + + public IEnumerable LanguageFallbacks + { + get { return languageFallbacks; } + } + + string IFieldPartitionItem.Key + { + get { return language.Iso2Code; } + } + + string IFieldPartitionItem.Name + { + get { return language.EnglishName; } + } + + IEnumerable IFieldPartitionItem.Fallback + { + get { return LanguageFallbacks.Select(x => x.Iso2Code); } + } + + public LanguageConfig(Language language, bool isOptional = false, IEnumerable fallback = null) + : this(language, isOptional, fallback?.ToArray()) + { + } + + public LanguageConfig(Language language, bool isOptional = false, params Language[] fallback) + { + Guard.NotNull(language, nameof(language)); + + this.isOptional = isOptional; + + this.language = language; + this.languageFallbacks = fallback ?? new Language[0]; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs new file mode 100644 index 000000000..c1d8fb2c8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs @@ -0,0 +1,172 @@ +// ========================================================================== +// LanguagesConfig.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core +{ + public sealed class LanguagesConfig : IFieldPartitioning + { + private State state; + + public LanguageConfig Master + { + get { return state.Master; } + } + + public int Count + { + get { return state.Languages.Count; } + } + + IFieldPartitionItem IFieldPartitioning.Master + { + get { return state.Master; } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return state.Languages.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return state.Languages.Values.GetEnumerator(); + } + + private LanguagesConfig(ICollection configs) + { + Guard.NotNull(configs, nameof(configs)); + + state = new State(configs.ToImmutableDictionary(x => x.Language), configs.FirstOrDefault()); + } + + public static LanguagesConfig Build(params LanguageConfig[] configs) + { + Guard.NotNull(configs, nameof(configs)); + + return new LanguagesConfig(configs); + } + + public static LanguagesConfig Build(params Language[] languages) + { + Guard.NotNull(languages, nameof(languages)); + + return new LanguagesConfig(languages.Select(x => new LanguageConfig(x, false)).ToList()); + } + + public void MakeMaster(Language language) + { + Guard.NotNull(language, nameof(language)); + + state = new State(state.Languages, state.Languages[language]); + } + + public void Set(LanguageConfig config) + { + Guard.NotNull(config, nameof(config)); + + state = new State(state.Languages.SetItem(config.Language, config), state.Master?.Language == config.Language ? config : state.Master); + } + + public void Remove(Language language) + { + Guard.NotNull(language, nameof(language)); + + state = new State( + state.Languages.Values.Where(x => x.Language != language) + .Select(config => + { + return new LanguageConfig( + config.Language, + config.IsOptional, + config.LanguageFallbacks.Except(new[] { language })); + }) + .ToImmutableDictionary(x => x.Language), state.Master.Language == language ? null : state.Master); + } + + public bool Contains(Language language) + { + return language != null && state.Languages.ContainsKey(language); + } + + public bool TryGetConfig(Language language, out LanguageConfig config) + { + return state.Languages.TryGetValue(language, out config); + } + + public bool TryGetItem(string key, out IFieldPartitionItem item) + { + item = null; + + if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value)) + { + item = value; + + return true; + } + + return false; + } + + private sealed class State + { + public ImmutableDictionary Languages { get; } + + public LanguageConfig Master { get; } + + public State(ImmutableDictionary languages, LanguageConfig master) + { + foreach (var languageConfig in languages.Values) + { + foreach (var fallback in languageConfig.LanguageFallbacks) + { + if (!languages.ContainsKey(fallback)) + { + var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; + + throw new InvalidOperationException(message); + } + } + } + + Languages = languages; + + if (master == null) + { + throw new InvalidOperationException("Config has no master language."); + } + + if (master.IsOptional) + { + throw new InvalidOperationException("Config has an optional master language."); + } + + this.Master = master; + } + } + + public PartitionResolver ToResolver() + { + return partitioning => + { + if (partitioning.Equals(Partitioning.Invariant)) + { + return InvariantPartitioning.Instance; + } + + return this; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs new file mode 100644 index 000000000..74f2ea770 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// Partitioning.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core +{ + public delegate IFieldPartitioning PartitionResolver(Partitioning key); + + public sealed class Partitioning : IEquatable + { + public static readonly Partitioning Invariant = new Partitioning("invariant"); + public static readonly Partitioning Language = new Partitioning("language"); + + public string Key { get; } + + public Partitioning(string key) + { + Guard.NotNullOrEmpty(key, nameof(key)); + + Key = key; + } + + public override bool Equals(object obj) + { + return Equals(obj as Partitioning); + } + + public bool Equals(Partitioning other) + { + return string.Equals(other?.Key, Key, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return Key.GetHashCode(); + } + + public override string ToString() + { + return Key; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs new file mode 100644 index 000000000..3b9d9deae --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// AssetsField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class AssetsField : Field + { + public AssetsField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new AssetsFieldProperties()) + { + } + + public AssetsField(long id, string name, Partitioning partitioning, AssetsFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs new file mode 100644 index 000000000..cf1ad40a6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// AssetsFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(AssetsField))] + public sealed class AssetsFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs new file mode 100644 index 000000000..70dc99448 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// BooleanField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class BooleanField : Field + { + public BooleanField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new BooleanFieldProperties()) + { + } + + public BooleanField(long id, string name, Partitioning partitioning, BooleanFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs new file mode 100644 index 000000000..6be38b6f9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldEditor.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// BooleanFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum BooleanFieldEditor + { + Checkbox, + Toggle + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs new file mode 100644 index 000000000..f773ce620 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// BooleanFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(BooleanField))] + public sealed class BooleanFieldProperties : FieldProperties + { + public bool? DefaultValue { get; set; } + + public BooleanFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs new file mode 100644 index 000000000..0e71b0187 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeCalculatedDefaultValue.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// DateTimeCalculatedDefaultValue.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum DateTimeCalculatedDefaultValue + { + Now, + Today + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs new file mode 100644 index 000000000..f3b1ba6fc --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// DateTimeField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class DateTimeField : Field + { + public DateTimeField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new DateTimeFieldProperties()) + { + } + + public DateTimeField(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs new file mode 100644 index 000000000..cb2be2f19 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldEditor.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// DateTimeFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum DateTimeFieldEditor + { + Date, + DateTime + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs new file mode 100644 index 000000000..9c03a46c8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// DateTimeFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(DateTimeField))] + public sealed class DateTimeFieldProperties : FieldProperties + { + public Instant? MaxValue { get; set; } + + public Instant? MinValue { get; set; } + + public Instant? DefaultValue { get; set; } + + public DateTimeFieldEditor Editor { get; set; } + + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs new file mode 100644 index 000000000..2f9edf612 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Field.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class Field + { + private readonly long fieldId; + private readonly Partitioning partitioning; + private readonly string fieldName; + private bool isDisabled; + private bool isHidden; + private bool isLocked; + + public long Id + { + get { return fieldId; } + } + + public string Name + { + get { return fieldName; } + } + + public bool IsLocked + { + get { return isLocked; } + } + + public bool IsHidden + { + get { return isHidden; } + } + + public bool IsDisabled + { + get { return isDisabled; } + } + + public Partitioning Partitioning + { + get { return partitioning; } + } + + public abstract FieldProperties RawProperties { get; } + + protected Field(long id, string name, Partitioning partitioning) + { + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNull(partitioning, nameof(partitioning)); + Guard.GreaterThan(id, 0, nameof(id)); + + fieldId = id; + fieldName = name; + + this.partitioning = partitioning; + } + + public void Lock() + { + isLocked = true; + } + + public void Hide() + { + isHidden = true; + } + + public void Show() + { + isHidden = false; + } + + public void Disable() + { + isDisabled = true; + } + + public void Enable() + { + isDisabled = false; + } + + public abstract void Update(FieldProperties newProperties); + + public abstract T Accept(IFieldVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs new file mode 100644 index 000000000..334d5b59e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// FieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class FieldProperties : NamedElementPropertiesBase + { + public bool IsRequired { get; set; } + + public bool IsListField { get; set; } + + public string Placeholder { get; set; } + + public abstract T Accept(IFieldPropertiesVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs new file mode 100644 index 000000000..bf94a3a13 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// FieldRegistry.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class FieldRegistry + { + private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties); + + private readonly TypeNameRegistry typeNameRegistry; + private readonly Dictionary fieldsByPropertyType = new Dictionary(); + + private sealed class Registered + { + private readonly FactoryFunction fieldFactory; + private readonly Type propertiesType; + + public Type PropertiesType + { + get { return propertiesType; } + } + + public Registered(FactoryFunction fieldFactory, Type propertiesType) + { + this.fieldFactory = fieldFactory; + this.propertiesType = propertiesType; + } + + public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) + { + return fieldFactory(id, name, partitioning, properties); + } + } + + public FieldRegistry(TypeNameRegistry typeNameRegistry) + { + Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry)); + + this.typeNameRegistry = typeNameRegistry; + + Add( + (id, name, partitioning, properties) => + new BooleanField(id, name, partitioning, (BooleanFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new NumberField(id, name, partitioning, (NumberFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new StringField(id, name, partitioning, (StringFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new JsonField(id, name, partitioning, (JsonFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new AssetsField(id, name, partitioning, (AssetsFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new GeolocationField(id, name, partitioning, (GeolocationFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new ReferencesField(id, name, partitioning, (ReferencesFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new DateTimeField(id, name, partitioning, (DateTimeFieldProperties)properties)); + + Add( + (id, name, partitioning, properties) => + new TagsField(id, name, partitioning, (TagsFieldProperties)properties)); + + typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime"); + + typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References"); + } + + private void Add(FactoryFunction fieldFactory) + { + Guard.NotNull(fieldFactory, nameof(fieldFactory)); + + typeNameRegistry.Map(typeof(TFieldProperties)); + + var registered = new Registered(fieldFactory, typeof(TFieldProperties)); + + fieldsByPropertyType[registered.PropertiesType] = registered; + } + + public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties) + { + Guard.NotNull(properties, nameof(properties)); + + var registered = fieldsByPropertyType.GetOrDefault(properties.GetType()); + + if (registered == null) + { + throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported."); + } + + return registered.CreateField(id, name, partitioning, properties); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs new file mode 100644 index 000000000..edc05dbb2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Field_Generic.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class Field : Field where T : FieldProperties, new() + { + private T properties; + + public T Properties + { + get { return properties; } + } + + public override FieldProperties RawProperties + { + get { return properties; } + } + + protected Field(long id, string name, Partitioning partitioning, T properties) + : base(id, name, partitioning) + { + Guard.NotNull(properties, nameof(properties)); + + this.properties = properties; + } + + public override void Update(FieldProperties newProperties) + { + var typedProperties = ValidateProperties(newProperties); + + properties = typedProperties; + } + + private T ValidateProperties(FieldProperties newProperties) + { + Guard.NotNull(newProperties, nameof(newProperties)); + + if (!(newProperties is T typedProperties)) + { + throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties)); + } + + return typedProperties; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs new file mode 100644 index 000000000..fc5b313a0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// GeolocationField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class GeolocationField : Field + { + public GeolocationField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new GeolocationFieldProperties()) + { + } + + public GeolocationField(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs new file mode 100644 index 000000000..d6f3acff7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldEditor.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// GeolocationFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum GeolocationFieldEditor + { + Map + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..5e4b34d4e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// GeolocationFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(GeolocationField))] + public sealed class GeolocationFieldProperties : FieldProperties + { + public GeolocationFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs new file mode 100644 index 000000000..97087296a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// IFieldPropertiesVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public interface IFieldPropertiesVisitor + { + T Visit(AssetsFieldProperties properties); + + T Visit(BooleanFieldProperties properties); + + T Visit(DateTimeFieldProperties properties); + + T Visit(GeolocationFieldProperties properties); + + T Visit(JsonFieldProperties properties); + + T Visit(NumberFieldProperties properties); + + T Visit(ReferencesFieldProperties properties); + + T Visit(StringFieldProperties properties); + + T Visit(TagsFieldProperties properties); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs new file mode 100644 index 000000000..eecd20a73 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// IFieldVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public interface IFieldVisitor + { + T Visit(AssetsField field); + + T Visit(BooleanField field); + + T Visit(DateTimeField field); + + T Visit(GeolocationField field); + + T Visit(JsonField field); + + T Visit(NumberField field); + + T Visit(ReferencesField field); + + T Visit(StringField field); + + T Visit(TagsField field); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs new file mode 100644 index 000000000..51b103012 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// JsonFieldModel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonFieldModel + { + [JsonProperty] + public long Id { get; set; } + + [JsonProperty] + public bool IsHidden { get; set; } + + [JsonProperty] + public bool IsLocked { get; set; } + + [JsonProperty] + public bool IsDisabled { get; set; } + + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Partitioning { get; set; } + + [JsonProperty] + public FieldProperties Properties { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs new file mode 100644 index 000000000..f2c6695bd --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// JsonSchemaModel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class JsonSchemaModel + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public bool IsPublished { get; set; } + + [JsonProperty] + public SchemaProperties Properties { get; set; } + + [JsonProperty] + public List Fields { get; set; } + + public JsonSchemaModel() + { + } + + public JsonSchemaModel(Schema schema) + { + Name = schema.Name; + + Properties = schema.Properties; + + Fields = + schema.Fields?.Select(x => + new JsonFieldModel + { + Id = x.Id, + Name = x.Name, + IsHidden = x.IsHidden, + IsLocked = x.IsLocked, + IsDisabled = x.IsDisabled, + Partitioning = x.Partitioning.Key, + Properties = x.RawProperties + }).ToList(); + + IsPublished = schema.IsPublished; + } + + public Schema ToSchema(FieldRegistry fieldRegistry) + { + var schema = new Schema(Name); + + if (Fields != null) + { + foreach (var fieldModel in Fields) + { + var parititonKey = new Partitioning(fieldModel.Partitioning); + + var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties); + + if (fieldModel.IsDisabled) + { + field.Disable(); + } + + if (fieldModel.IsLocked) + { + field.Lock(); + } + + if (fieldModel.IsHidden) + { + field.Hide(); + } + + schema.AddField(field); + } + } + + if (IsPublished) + { + schema.Publish(); + } + + if (Properties != null) + { + schema.Update(Properties); + } + + return schema; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs new file mode 100644 index 000000000..5502fd3f9 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// SchemaConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas.Json +{ + public sealed class SchemaConverter : JsonConverter + { + private readonly FieldRegistry fieldRegistry; + + public SchemaConverter(FieldRegistry fieldRegistry) + { + Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); + + this.fieldRegistry = fieldRegistry; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, new JsonSchemaModel((Schema)value)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader).ToSchema(fieldRegistry); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Schema); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs new file mode 100644 index 000000000..49fab3979 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// JsonField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class JsonField : Field + { + public JsonField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new JsonFieldProperties()) + { + } + + public JsonField(long id, string name, Partitioning partitioning, JsonFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs new file mode 100644 index 000000000..c432be09a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// JsonFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(JsonField))] + public sealed class JsonFieldProperties : FieldProperties + { + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs new file mode 100644 index 000000000..dfcbba099 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// NamedElementPropertiesBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public abstract class NamedElementPropertiesBase + { + public string Label { get; set; } + + public string Hints { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs new file mode 100644 index 000000000..d19b05c39 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// NumberField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class NumberField : Field + { + public NumberField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new NumberFieldProperties()) + { + } + + public NumberField(long id, string name, Partitioning partitioning, NumberFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs new file mode 100644 index 000000000..28f164ef8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldEditor.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// NumberFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum NumberFieldEditor + { + Input, + Radio, + Dropdown, + Stars + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs new file mode 100644 index 000000000..ce1703faa --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// NumberFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(NumberField))] + public sealed class NumberFieldProperties : FieldProperties + { + public double? MaxValue { get; set; } + + public double? MinValue { get; set; } + + public double? DefaultValue { get; set; } + + public double[] AllowedValues { get; set; } + + public NumberFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs new file mode 100644 index 000000000..f2dc2bc5b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// ReferencesField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class ReferencesField : Field + { + public ReferencesField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new ReferencesFieldProperties()) + { + } + + public ReferencesField(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs new file mode 100644 index 000000000..8a4f76192 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// ReferencesFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(ReferencesField))] + public sealed class ReferencesFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public Guid SchemaId { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs new file mode 100644 index 000000000..8a7a06d40 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -0,0 +1,120 @@ +// ========================================================================== +// Schema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class Schema + { + private readonly string name; + private readonly List fieldsOrdered = new List(); + private readonly Dictionary fieldsById = new Dictionary(); + private readonly Dictionary fieldsByName = new Dictionary(); + private SchemaProperties properties = new SchemaProperties(); + private bool isPublished; + + public string Name + { + get { return name; } + } + + public bool IsPublished + { + get { return isPublished; } + } + + public IReadOnlyList Fields + { + get { return fieldsOrdered; } + } + + public IReadOnlyDictionary FieldsById + { + get { return fieldsById; } + } + + public IReadOnlyDictionary FieldsByName + { + get { return fieldsByName; } + } + + public SchemaProperties Properties + { + get { return properties; } + } + + public void Publish() + { + isPublished = true; + } + + public void Unpublish() + { + isPublished = false; + } + + public Schema(string name) + { + Guard.NotNullOrEmpty(name, nameof(name)); + + this.name = name; + } + + public void Update(SchemaProperties newProperties) + { + Guard.NotNull(newProperties, nameof(newProperties)); + + properties = newProperties; + } + + public void DeleteField(long fieldId) + { + if (!fieldsById.TryGetValue(fieldId, out var field)) + { + return; + } + + fieldsById.Remove(fieldId); + fieldsByName.Remove(field.Name); + fieldsOrdered.Remove(field); + } + + public void ReorderFields(List ids) + { + Guard.NotNull(ids, nameof(ids)); + + if (ids.Count != fieldsOrdered.Count || ids.Any(x => !fieldsById.ContainsKey(x))) + { + throw new ArgumentException("Ids must cover all fields.", nameof(ids)); + } + + var fields = fieldsOrdered.ToList(); + + fieldsOrdered.Clear(); + fieldsOrdered.AddRange(fields.OrderBy(f => ids.IndexOf(f.Id))); + } + + public void AddField(Field field) + { + Guard.NotNull(field, nameof(field)); + + if (fieldsByName.ContainsKey(field.Name) || fieldsById.ContainsKey(field.Id)) + { + throw new ArgumentException($"A field with name '{field.Name}' already exists.", nameof(field)); + } + + fieldsById.Add(field.Id, field); + fieldsByName.Add(field.Name, field); + fieldsOrdered.Add(field); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs new file mode 100644 index 000000000..4731958e7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// SchemaProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class SchemaProperties : NamedElementPropertiesBase + { + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs new file mode 100644 index 000000000..d2587fd79 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// StringField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class StringField : Field + { + public StringField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new StringFieldProperties()) + { + } + + public StringField(long id, string name, Partitioning partitioning, StringFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs new file mode 100644 index 000000000..1226fef19 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// StringFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum StringFieldEditor + { + Input, + Markdown, + Dropdown, + Radio, + RichText, + TextArea + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs new file mode 100644 index 000000000..5263cc2e7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// StringFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(StringField))] + public sealed class StringFieldProperties : FieldProperties + { + public int? MinLength { get; set; } + + public int? MaxLength { get; set; } + + public string DefaultValue { get; set; } + + public string Pattern { get; set; } + + public string PatternMessage { get; set; } + + public string[] AllowedValues { get; set; } + + public StringFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs new file mode 100644 index 000000000..a36d061f1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsField.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// TagsField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class TagsField : Field + { + public TagsField(long id, string name, Partitioning partitioning) + : base(id, name, partitioning, new TagsFieldProperties()) + { + } + + public TagsField(long id, string name, Partitioning partitioning, TagsFieldProperties properties) + : base(id, name, partitioning, properties) + { + } + + public override T Accept(IFieldVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs new file mode 100644 index 000000000..9e10e634b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// TagsField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [TypeName(nameof(TagsField))] + public sealed class TagsFieldProperties : FieldProperties + { + public int? MinItems { get; set; } + + public int? MaxItems { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj new file mode 100644 index 000000000..754774561 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -0,0 +1,20 @@ + + + netstandard2.0 + Squidex.Domain.Apps.Core + + + full + True + + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs new file mode 100644 index 000000000..169e85b7d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Webhooks/WebhookSchema.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// WebhookSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Core.Webhooks +{ + public sealed class WebhookSchema + { + public Guid SchemaId { get; set; } + + public bool SendCreate { get; set; } + + public bool SendUpdate { get; set; } + + public bool SendDelete { get; set; } + + public bool SendPublish { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs new file mode 100644 index 000000000..30edad325 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -0,0 +1,201 @@ +// ========================================================================== +// ContentConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.ConvertContent +{ + public static class ContentConverter + { + public static NamedContentData ToNameModel(this IdContentData source, Schema schema, bool decodeJsonField) + { + Guard.NotNull(schema, nameof(schema)); + + var result = new NamedContentData(); + + foreach (var fieldValue in source) + { + if (!schema.FieldsById.TryGetValue(fieldValue.Key, out var field)) + { + continue; + } + + if (decodeJsonField && field is JsonField) + { + var encodedValue = new ContentFieldData(); + + foreach (var partitionValue in fieldValue.Value) + { + if (partitionValue.Value.IsNull()) + { + encodedValue[partitionValue.Key] = null; + } + else + { + var value = Encoding.UTF8.GetString(Convert.FromBase64String(partitionValue.Value.ToString())); + + encodedValue[partitionValue.Key] = JToken.Parse(value); + } + } + + result[field.Name] = encodedValue; + } + else + { + result[field.Name] = fieldValue.Value; + } + } + + return result; + } + + public static IdContentData ToIdModel(this NamedContentData content, Schema schema, bool encodeJsonField) + { + Guard.NotNull(schema, nameof(schema)); + + var result = new IdContentData(); + + foreach (var fieldValue in content) + { + if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field)) + { + continue; + } + + var fieldId = field.Id; + + if (encodeJsonField && field is JsonField) + { + var encodedValue = new ContentFieldData(); + + foreach (var partitionValue in fieldValue.Value) + { + if (partitionValue.Value.IsNull()) + { + encodedValue[partitionValue.Key] = null; + } + else + { + var value = Convert.ToBase64String(Encoding.UTF8.GetBytes(partitionValue.Value.ToString())); + + encodedValue[partitionValue.Key] = value; + } + } + + result[fieldId] = encodedValue; + } + else + { + result[fieldId] = fieldValue.Value; + } + } + + return result; + } + + public static NamedContentData ToApiModel(this NamedContentData content, Schema schema, LanguagesConfig languagesConfig, bool excludeHidden = true) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(languagesConfig, nameof(languagesConfig)); + + var codeForInvariant = InvariantPartitioning.Instance.Master.Key; + var codeForMasterLanguage = languagesConfig.Master.Language.Iso2Code; + + var result = new NamedContentData(); + + foreach (var fieldValue in content) + { + if (!schema.FieldsByName.TryGetValue(fieldValue.Key, out var field) || (excludeHidden && field.IsHidden)) + { + continue; + } + + var fieldResult = new ContentFieldData(); + var fieldValues = fieldValue.Value; + + if (field.Partitioning.Equals(Partitioning.Language)) + { + foreach (var languageConfig in languagesConfig) + { + var languageCode = languageConfig.Key; + + if (fieldValues.TryGetValue(languageCode, out var value)) + { + fieldResult.Add(languageCode, value); + } + else if (languageConfig == languagesConfig.Master && fieldValues.TryGetValue(codeForInvariant, out value)) + { + fieldResult.Add(languageCode, value); + } + } + } + else + { + if (fieldValues.TryGetValue(codeForInvariant, out var value)) + { + fieldResult.Add(codeForInvariant, value); + } + else if (fieldValues.TryGetValue(codeForMasterLanguage, out value)) + { + fieldResult.Add(codeForInvariant, value); + } + else if (fieldValues.Count > 0) + { + fieldResult.Add(codeForInvariant, fieldValues.Values.First()); + } + } + + result.Add(field.Name, fieldResult); + } + + return result; + } + + public static object ToLanguageModel(this NamedContentData content, LanguagesConfig languagesConfig, IReadOnlyCollection languagePreferences = null) + { + Guard.NotNull(languagesConfig, nameof(languagesConfig)); + + if (languagePreferences == null || languagePreferences.Count == 0) + { + return content; + } + + if (languagePreferences.Count == 1 && languagesConfig.TryGetConfig(languagePreferences.First(), out var languageConfig)) + { + languagePreferences = languagePreferences.Union(languageConfig.LanguageFallbacks).ToList(); + } + + var result = new Dictionary(); + + foreach (var fieldValue in content) + { + var fieldValues = fieldValue.Value; + + foreach (var language in languagePreferences) + { + if (fieldValues.TryGetValue(language, out var value) && value != null) + { + result[fieldValue.Key] = value; + + break; + } + } + } + + return result; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs new file mode 100644 index 000000000..10f73cb5e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// ContentEnricher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class ContentEnricher + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + + public ContentEnricher(Schema schema, PartitionResolver partitionResolver) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + + this.schema = schema; + + this.partitionResolver = partitionResolver; + } + + public void Enrich(NamedContentData data) + { + Guard.NotNull(data, nameof(data)); + + foreach (var field in schema.Fields) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + var fieldPartition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in fieldPartition) + { + Enrich(field, fieldData, partitionItem); + } + + if (fieldData.Count > 0) + { + data[field.Name] = fieldData; + } + } + } + + private static void Enrich(Field field, ContentFieldData fieldData, IFieldPartitionItem partitionItem) + { + Guard.NotNull(fieldData, nameof(fieldData)); + + var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); + + if (field.RawProperties.IsRequired || defaultValue.IsNull()) + { + return; + } + + var key = partitionItem.Key; + + if (!fieldData.TryGetValue(key, out var value) || ShouldApplyDefaultValue(field, value)) + { + fieldData.AddValue(key, defaultValue); + } + } + + private static bool ShouldApplyDefaultValue(Field field, JToken value) + { + return value.IsNull() || (field is StringField && value is JValue jValue && Equals(jValue.Value, string.Empty)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs new file mode 100644 index 000000000..68a5e044d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnrichmentExtensions.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// ContentExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public static class ContentEnrichmentExtensions + { + public static void Enrich(this NamedContentData data, Schema schema, PartitionResolver partitionResolver) + { + var enricher = new ContentEnricher(schema, partitionResolver); + + enricher.Enrich(data); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs new file mode 100644 index 000000000..d7529af47 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -0,0 +1,88 @@ +// ========================================================================== +// ValidatorsFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.EnrichContent +{ + public sealed class DefaultValueFactory : IFieldPropertiesVisitor + { + private readonly Instant now; + + private DefaultValueFactory(Instant now) + { + this.now = now; + } + + public static JToken CreateDefaultValue(Field field, Instant now) + { + Guard.NotNull(field, nameof(field)); + + return field.RawProperties.Accept(new DefaultValueFactory(now)); + } + + public JToken Visit(AssetsFieldProperties properties) + { + return new JArray(); + } + + public JToken Visit(BooleanFieldProperties properties) + { + return properties.DefaultValue; + } + + public JToken Visit(GeolocationFieldProperties properties) + { + return JValue.CreateNull(); + } + + public JToken Visit(JsonFieldProperties properties) + { + return JValue.CreateNull(); + } + + public JToken Visit(NumberFieldProperties properties) + { + return properties.DefaultValue; + } + + public JToken Visit(ReferencesFieldProperties properties) + { + return new JArray(); + } + + public JToken Visit(StringFieldProperties properties) + { + return properties.DefaultValue; + } + + public JToken Visit(TagsFieldProperties properties) + { + return new JArray(); + } + + public JToken Visit(DateTimeFieldProperties properties) + { + if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) + { + return now.ToString(); + } + + if (properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) + { + return now.ToString().Substring(10); + } + + return properties.DefaultValue?.ToString(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs new file mode 100644 index 000000000..d5813b87f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ContentReferencesExtensions.cs @@ -0,0 +1,75 @@ +// ========================================================================== +// ContentReferencesExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ContentReferencesExtensions + { + public static IdContentData ToCleanedReferences(this IdContentData source, Schema schema, ISet deletedReferencedIds) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(deletedReferencedIds, nameof(deletedReferencedIds)); + + var result = new IdContentData(source); + + foreach (var field in schema.Fields) + { + var fieldData = source.GetOrDefault(field.Id); + + if (fieldData == null) + { + continue; + } + + foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull()).ToList()) + { + var newValue = field.CleanReferences(partitionValue.Value, deletedReferencedIds); + + fieldData[partitionValue.Key] = newValue; + } + } + + return result; + } + + public static IEnumerable GetReferencedIds(this IdContentData source, Schema schema) + { + Guard.NotNull(schema, nameof(schema)); + + var foundReferences = new HashSet(); + + foreach (var field in schema.Fields) + { + var fieldData = source.GetOrDefault(field.Id); + + if (fieldData == null) + { + continue; + } + + foreach (var partitionValue in fieldData.Where(x => !x.Value.IsNull())) + { + var ids = field.ExtractReferences(partitionValue.Value); + + foreach (var id in ids.Where(x => foundReferences.Add(x))) + { + yield return id; + } + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs new file mode 100644 index 000000000..31f67410f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// ReferenceExtractor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ReferencesCleaner + { + public static JToken CleanReferences(this Field field, JToken value, ISet oldReferences) + { + if ((field is AssetsField || field is ReferencesField) && !value.IsNull()) + { + switch (field) + { + case AssetsField assetsField: + return Visit(assetsField, value, oldReferences); + + case ReferencesField referencesField: + return Visit(referencesField, value, oldReferences); + } + } + + return value; + } + + private static JToken Visit(AssetsField field, JToken value, IEnumerable oldReferences) + { + var oldIds = field.ExtractReferences(value).ToList(); + var newIds = oldIds.Except(oldReferences).ToList(); + + return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value; + } + + private static JToken Visit(ReferencesField field, JToken value, ICollection oldReferences) + { + if (oldReferences.Contains(field.Properties.SchemaId)) + { + return new JArray(); + } + + var oldIds = field.ExtractReferences(value).ToList(); + var newIds = oldIds.Except(oldReferences).ToList(); + + return oldIds.Count != newIds.Count ? JToken.FromObject(newIds) : value; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs new file mode 100644 index 000000000..f70c8b9f0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// ReferenceExtractor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.ExtractReferenceIds +{ + public static class ReferencesExtractor + { + public static IEnumerable ExtractReferences(this Field field, JToken value) + { + switch (field) + { + case AssetsField assetsField: + return Visit(assetsField, value); + + case ReferencesField referencesField: + return Visit(referencesField, value); + } + + return Enumerable.Empty(); + } + + public static IEnumerable Visit(AssetsField field, JToken value) + { + IEnumerable result = null; + try + { + result = value?.ToObject>(); + } + catch + { + result = null; + } + + return result ?? Enumerable.Empty(); + } + + private static IEnumerable Visit(ReferencesField field, JToken value) + { + IEnumerable result = null; + try + { + result = value?.ToObject>() ?? Enumerable.Empty(); + } + catch + { + result = Enumerable.Empty(); + } + + return result.Union(new[] { field.Properties.SchemaId }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs new file mode 100644 index 000000000..767f89839 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// EdmSchemaExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public static class EdmSchemaExtensions + { + public static string EscapeEdmField(this string field) + { + return field.Replace("-", "_"); + } + + public static string UnescapeEdmField(this string field) + { + return field.Replace("_", "-"); + } + + public static EdmComplexType BuildEdmType(this Schema schema, PartitionResolver partitionResolver, Func typeResolver) + { + Guard.NotNull(typeResolver, nameof(typeResolver)); + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + + var schemaName = schema.Name.ToPascalCase(); + + var edmType = new EdmComplexType("Squidex", schemaName); + + foreach (var field in schema.FieldsByName.Values.Where(x => !x.IsHidden)) + { + var edmValueType = EdmTypeVisitor.CreateEdmType(field); + + if (edmValueType == null) + { + continue; + } + + var partitionType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{field.Name.ToPascalCase()}Property")); + var partition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partition) + { + partitionType.AddStructuralProperty(partitionItem.Key, edmValueType); + } + + edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); + } + + return edmType; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs new file mode 100644 index 000000000..c80d94fc1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// EdmTypeVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.OData.Edm; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.GenerateEdmSchema +{ + public sealed class EdmTypeVisitor : IFieldVisitor + { + private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor(); + + private EdmTypeVisitor() + { + } + + public static IEdmTypeReference CreateEdmType(Field field) + { + return field.Accept(Instance); + } + + public IEdmTypeReference Visit(AssetsField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference Visit(BooleanField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Boolean, field); + } + + public IEdmTypeReference Visit(DateTimeField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.DateTimeOffset, field); + } + + public IEdmTypeReference Visit(GeolocationField field) + { + return null; + } + + public IEdmTypeReference Visit(JsonField field) + { + return null; + } + + public IEdmTypeReference Visit(NumberField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.Double, field); + } + + public IEdmTypeReference Visit(ReferencesField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference Visit(StringField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + public IEdmTypeReference Visit(TagsField field) + { + return CreatePrimitive(EdmPrimitiveTypeKind.String, field); + } + + private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, Field field) + { + return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs new file mode 100644 index 000000000..9ba399359 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// ContentSchemaBuilder.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public sealed class ContentSchemaBuilder + { + public JsonSchema4 CreateContentSchema(Schema schema, JsonSchema4 dataSchema) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(dataSchema, nameof(dataSchema)); + + var schemaName = schema.Properties.Label.WithFallback(schema.Name); + + var contentSchema = new JsonSchema4 + { + Properties = + { + ["id"] = CreateProperty($"The id of the {schemaName} content."), + ["data"] = CreateProperty($"The data of the {schemaName}.", dataSchema), + ["version"] = CreateProperty($"The version of the {schemaName}.", JsonObjectType.Number), + ["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"), + ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."), + ["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"), + ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") + }, + Type = JsonObjectType.Object + }; + + return contentSchema; + } + + private static JsonProperty CreateProperty(string description, JsonSchema4 dataSchema) + { + return new JsonProperty { Description = description, IsRequired = true, Type = JsonObjectType.Object, Reference = dataSchema }; + } + + private static JsonProperty CreateProperty(string description, JsonObjectType type) + { + return new JsonProperty { Description = description, IsRequired = true, Type = type }; + } + + private static JsonProperty CreateProperty(string description, string format = null) + { + return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs new file mode 100644 index 000000000..d20a28607 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// JsonSchemaExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class JsonSchemaExtensions + { + public static JsonSchema4 BuildJsonSchema(this Schema schema, PartitionResolver partitionResolver, Func schemaResolver) + { + Guard.NotNull(schemaResolver, nameof(schemaResolver)); + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + + var schemaName = schema.Name.ToPascalCase(); + + var jsonTypeVisitor = new JsonTypeVisitor(schemaResolver); + var jsonSchema = new JsonSchema4 { Type = JsonObjectType.Object }; + + foreach (var field in schema.Fields.Where(x => !x.IsHidden)) + { + var partitionProperty = CreateProperty(field); + var partitionObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false }; + var partition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partition) + { + var partitionItemProperty = field.Accept(jsonTypeVisitor); + + partitionItemProperty.Description = partitionItem.Name; + partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); + } + + partitionProperty.Reference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); + + jsonSchema.Properties.Add(field.Name, partitionProperty); + } + + return jsonSchema; + } + + public static JsonProperty CreateProperty(Field field) + { + var jsonProperty = new JsonProperty { IsRequired = field.RawProperties.IsRequired, Type = JsonObjectType.Object }; + + if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) + { + jsonProperty.Description = field.RawProperties.Hints; + } + else + { + jsonProperty.Description = field.Name; + } + + if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) + { + jsonProperty.Description += $" ({field.RawProperties.Hints})."; + } + + return jsonProperty; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs new file mode 100644 index 000000000..a2c3b6f7d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -0,0 +1,163 @@ +// ========================================================================== +// JsonTypeVisitor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.ObjectModel; +using NJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public sealed class JsonTypeVisitor : IFieldVisitor + { + private readonly Func schemaResolver; + + public JsonTypeVisitor(Func schemaResolver) + { + this.schemaResolver = schemaResolver; + } + + public JsonProperty Visit(AssetsField field) + { + return CreateProperty(field, jsonProperty => + { + var itemSchema = schemaResolver("AssetItem", new JsonSchema4 { Type = JsonObjectType.String }); + + jsonProperty.Type = JsonObjectType.Array; + jsonProperty.Item = itemSchema; + }); + } + + public JsonProperty Visit(BooleanField field) + { + return CreateProperty(field, jsonProperty => + { + jsonProperty.Type = JsonObjectType.Boolean; + }); + } + + public JsonProperty Visit(DateTimeField field) + { + return CreateProperty(field, jsonProperty => + { + jsonProperty.Type = JsonObjectType.String; + jsonProperty.Format = JsonFormatStrings.DateTime; + }); + } + + public JsonProperty Visit(GeolocationField field) + { + return CreateProperty(field, jsonProperty => + { + var geolocationSchema = new JsonSchema4 + { + AllowAdditionalProperties = false + }; + + geolocationSchema.Properties.Add("latitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -90, + Maximum = 90, + IsRequired = true + }); + + geolocationSchema.Properties.Add("longitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -180, + Maximum = 180, + IsRequired = true + }); + + var schemaReference = schemaResolver("GeolocationDto", geolocationSchema); + + jsonProperty.Type = JsonObjectType.Object; + jsonProperty.Reference = schemaReference; + }); + } + + public JsonProperty Visit(JsonField field) + { + return CreateProperty(field, jsonProperty => + { + jsonProperty.Type = JsonObjectType.Object; + }); + } + + public JsonProperty Visit(NumberField field) + { + return CreateProperty(field, jsonProperty => + { + jsonProperty.Type = JsonObjectType.Number; + + if (field.Properties.MinValue.HasValue) + { + jsonProperty.Minimum = (decimal)field.Properties.MinValue.Value; + } + + if (field.Properties.MaxValue.HasValue) + { + jsonProperty.Maximum = (decimal)field.Properties.MaxValue.Value; + } + }); + } + + public JsonProperty Visit(ReferencesField field) + { + return CreateProperty(field, jsonProperty => + { + var itemSchema = schemaResolver("ReferenceItem", new JsonSchema4 { Type = JsonObjectType.String }); + + jsonProperty.Type = JsonObjectType.Array; + jsonProperty.Item = itemSchema; + }); + } + + public JsonProperty Visit(StringField field) + { + return CreateProperty(field, jsonProperty => + { + jsonProperty.Type = JsonObjectType.String; + + jsonProperty.MinLength = field.Properties.MinLength; + jsonProperty.MaxLength = field.Properties.MaxLength; + + if (field.Properties.AllowedValues != null) + { + var names = jsonProperty.EnumerationNames = jsonProperty.EnumerationNames ?? new Collection(); + + foreach (var value in field.Properties.AllowedValues) + { + names.Add(value); + } + } + }); + } + + public JsonProperty Visit(TagsField field) + { + return CreateProperty(field, jsonProperty => + { + var itemSchema = schemaResolver("TagsItem", new JsonSchema4 { Type = JsonObjectType.String }); + + jsonProperty.Type = JsonObjectType.Array; + jsonProperty.Item = itemSchema; + }); + } + + private static JsonProperty CreateProperty(Field field, Action updater) + { + var property = new JsonProperty { IsRequired = field.RawProperties.IsRequired }; + + updater(property); + + return property; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs new file mode 100644 index 000000000..8ceadc1ba --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// ContentDataObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataObject : ObjectInstance + { + private readonly NamedContentData contentData; + private HashSet fieldsToDelete; + private Dictionary fieldProperties; + private bool isChanged; + + public ContentDataObject(Engine engine, NamedContentData contentData) + : base(engine) + { + Extensible = true; + + this.contentData = contentData; + } + + public void MarkChanged() + { + isChanged = true; + } + + public bool TryUpdate(out NamedContentData result) + { + result = contentData; + + if (isChanged) + { + if (fieldsToDelete != null) + { + foreach (var field in fieldsToDelete) + { + contentData.Remove(field); + } + } + + if (fieldProperties != null) + { + foreach (var kvp in fieldProperties) + { + if (kvp.Value.ContentField.TryUpdate(out var fieldData)) + { + contentData[kvp.Key] = fieldData; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (fieldsToDelete == null) + { + fieldsToDelete = new HashSet(); + } + + fieldsToDelete.Add(propertyName); + fieldProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!fieldProperties.ContainsKey(propertyName)) + { + fieldProperties[propertyName] = new ContentDataProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override void Put(string propertyName, JsValue value, bool throwOnError) + { + EnsurePropertiesInitialized(); + + fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this)).Value = value; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true); + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + foreach (var property in fieldProperties) + { + yield return new KeyValuePair(property.Key, property.Value); + } + } + + private void EnsurePropertiesInitialized() + { + if (fieldProperties == null) + { + fieldProperties = new Dictionary(contentData.Count); + + foreach (var kvp in contentData) + { + fieldProperties.Add(kvp.Key, new ContentDataProperty(this, new ContentFieldObject(this, kvp.Value, false))); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs new file mode 100644 index 000000000..6542d772b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs @@ -0,0 +1,68 @@ +// ========================================================================== +// ContentFieldProperty.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Jint.Native; +using Jint.Runtime; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentDataProperty : PropertyDescriptor + { + private readonly ContentDataObject contentData; + private ContentFieldObject contentField; + private JsValue value; + + public override JsValue Value + { + get + { + return value; + } + set + { + if (!Equals(this.value, value)) + { + if (value == null || !value.IsObject()) + { + throw new JavaScriptException("Can only assign object to content data."); + } + + var obj = value.AsObject(); + + contentField = new ContentFieldObject(contentData, new ContentFieldData(), true); + + foreach (var kvp in obj.GetOwnProperties()) + { + contentField.Put(kvp.Key, kvp.Value.Value, true); + } + + this.value = new JsValue(contentField); + } + } + } + + public ContentFieldObject ContentField + { + get { return contentField; } + } + + public ContentDataProperty(ContentDataObject contentData, ContentFieldObject contentField = null) + : base(null, true, true, true) + { + this.contentData = contentData; + this.contentField = contentField; + + if (contentField != null) + { + value = new JsValue(contentField); + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs new file mode 100644 index 000000000..e4eed7641 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// ContentFieldObject.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Jint.Native.Object; +using Jint.Runtime.Descriptors; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; + +#pragma warning disable RECS0133 // Parameter name differs in base declaration + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldObject : ObjectInstance + { + private readonly ContentDataObject contentData; + private readonly ContentFieldData fieldData; + private HashSet valuesToDelete; + private Dictionary valueProperties; + private bool isChanged; + + public ContentFieldData FieldData + { + get { return fieldData; } + } + + public ContentFieldObject(ContentDataObject contentData, ContentFieldData fieldData, bool isNew) + : base(contentData.Engine) + { + Extensible = true; + + this.contentData = contentData; + this.fieldData = fieldData; + + if (isNew) + { + MarkChanged(); + } + } + + public void MarkChanged() + { + isChanged = true; + + contentData.MarkChanged(); + } + + public bool TryUpdate(out ContentFieldData result) + { + result = fieldData; + + if (isChanged) + { + if (valuesToDelete != null) + { + foreach (var field in valuesToDelete) + { + fieldData.Remove(field); + } + } + + if (valueProperties != null) + { + foreach (var kvp in valueProperties) + { + if (kvp.Value.IsChanged) + { + fieldData[kvp.Key] = kvp.Value.ContentValue; + } + } + } + } + + return isChanged; + } + + public override void RemoveOwnProperty(string propertyName) + { + if (valuesToDelete == null) + { + valuesToDelete = new HashSet(); + } + + valuesToDelete.Add(propertyName); + valueProperties?.Remove(propertyName); + + MarkChanged(); + } + + public override bool DefineOwnProperty(string propertyName, PropertyDescriptor desc, bool throwOnError) + { + EnsurePropertiesInitialized(); + + if (!valueProperties.ContainsKey(propertyName)) + { + valueProperties[propertyName] = new ContentFieldProperty(this) { Value = desc.Value }; + } + + return true; + } + + public override PropertyDescriptor GetOwnProperty(string propertyName) + { + EnsurePropertiesInitialized(); + + return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined; + } + + public override IEnumerable> GetOwnProperties() + { + EnsurePropertiesInitialized(); + + foreach (var property in valueProperties) + { + yield return new KeyValuePair(property.Key, property.Value); + } + } + + private void EnsurePropertiesInitialized() + { + if (valueProperties == null) + { + valueProperties = new Dictionary(FieldData.Count); + + foreach (var kvp in FieldData) + { + valueProperties.Add(kvp.Key, new ContentFieldProperty(this, kvp.Value)); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs new file mode 100644 index 000000000..01e50da58 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldProperty.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// ContentFieldProperty.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Jint.Native; +using Jint.Runtime.Descriptors; +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public sealed class ContentFieldProperty : PropertyDescriptor + { + private readonly ContentFieldObject contentField; + private JToken contentValue; + private JsValue value; + private bool isChanged; + + public override JsValue Value + { + get + { + return value ?? (value = JsonMapper.Map(contentValue, contentField.Engine)); + } + set + { + if (!Equals(this.value, value)) + { + this.value = value; + + contentValue = null; + contentField.MarkChanged(); + + isChanged = true; + } + } + } + + public JToken ContentValue + { + get { return contentValue ?? (contentValue = JsonMapper.Map(value)); } + } + + public bool IsChanged + { + get { return isChanged; } + } + + public ContentFieldProperty(ContentFieldObject contentField, JToken contentValue = null) + : base(null, true, true, true) + { + this.contentField = contentField; + this.contentValue = contentValue; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs new file mode 100644 index 000000000..6704c5440 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// JsonMapper.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Newtonsoft.Json.Linq; + +namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper +{ + public static class JsonMapper + { + public static JsValue Map(JToken value, Engine engine) + { + if (value == null) + { + return JsValue.Null; + } + + switch (value.Type) + { + case JTokenType.Date: + case JTokenType.Guid: + case JTokenType.String: + case JTokenType.Uri: + case JTokenType.TimeSpan: + return new JsValue((string)value); + case JTokenType.Null: + return JsValue.Null; + case JTokenType.Undefined: + return JsValue.Undefined; + case JTokenType.Integer: + return new JsValue((long)value); + case JTokenType.Float: + return new JsValue((double)value); + case JTokenType.Boolean: + return new JsValue((bool)value); + case JTokenType.Object: + return FromObject(value, engine); + case JTokenType.Array: + { + var arr = (JArray)value; + + var target = new JsValue[arr.Count]; + + for (var i = 0; i < arr.Count; i++) + { + target[i] = Map(arr[i], engine); + } + + return engine.Array.Construct(target); + } + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + + private static JsValue FromObject(JToken value, Engine engine) + { + var obj = (JObject)value; + + var target = new ObjectInstance(engine); + + foreach (var property in obj) + { + target.FastAddProperty(property.Key, Map(property.Value, engine), false, true, true); + } + + return target; + } + + public static JToken Map(JsValue value) + { + if (value == null || value.IsNull()) + { + return JValue.CreateNull(); + } + + if (value.IsUndefined()) + { + return JValue.CreateUndefined(); + } + + if (value.IsString()) + { + return new JValue(value.AsString()); + } + + if (value.IsBoolean()) + { + return new JValue(value.AsBoolean()); + } + + if (value.IsNumber()) + { + return new JValue(value.AsNumber()); + } + + if (value.IsDate()) + { + return new JValue(value.AsDate().ToDateTime()); + } + + if (value.IsRegExp()) + { + return JValue.CreateString(value.AsRegExp().Value?.ToString()); + } + + if (value.IsArray()) + { + var arr = value.AsArray(); + + var target = new JArray(); + + for (var i = 0; i < arr.GetLength(); i++) + { + target.Add(Map(arr.Get(i.ToString()))); + } + + return target; + } + + if (value.IsObject()) + { + var obj = value.AsObject(); + + var target = new JObject(); + + foreach (var kvp in obj.GetOwnProperties()) + { + target[kvp.Key] = Map(kvp.Value.Value); + } + + return target; + } + + throw new ArgumentException("Invalid json type.", nameof(value)); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs new file mode 100644 index 000000000..bad0351f6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IScriptEngine.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public interface IScriptEngine + { + void Execute(ScriptContext context, string script); + + NamedContentData ExecuteAndTransform(ScriptContext context, string script); + + NamedContentData Transform(ScriptContext context, string script); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs new file mode 100644 index 000000000..6ab62dc73 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -0,0 +1,178 @@ +// ========================================================================== +// JintScriptEngine.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Jint; +using Jint.Native.Object; +using Jint.Parser; +using Jint.Runtime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class JintScriptEngine : IScriptEngine + { + public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); + + public void Execute(ScriptContext context, string script) + { + Guard.NotNull(context, nameof(context)); + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + Execute(engine, script); + } + } + + public NamedContentData ExecuteAndTransform(ScriptContext context, string script) + { + Guard.NotNull(context, nameof(context)); + + var result = context.Data; + + if (!string.IsNullOrWhiteSpace(script)) + { + var engine = CreateScriptEngine(context); + + EnableDisallow(engine); + EnableReject(engine); + + engine.SetValue("operation", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + Execute(engine, script); + } + + return result; + } + + public NamedContentData Transform(ScriptContext context, string script) + { + Guard.NotNull(context, nameof(context)); + + var result = context.Data; + + if (!string.IsNullOrWhiteSpace(script)) + { + try + { + var engine = CreateScriptEngine(context); + + engine.SetValue("replace", new Action(() => + { + var dataInstance = engine.GetValue("ctx").AsObject().Get("data"); + + if (dataInstance != null && dataInstance.IsObject() && dataInstance.AsObject() is ContentDataObject data) + { + data.TryUpdate(out result); + } + })); + + engine.Execute(script); + } + catch (Exception) + { + result = context.Data; + } + } + + return result; + } + + private static void Execute(Engine engine, string script) + { + try + { + engine.Execute(script); + } + catch (ParserException ex) + { + throw new ValidationException($"Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message)); + } + catch (JavaScriptException ex) + { + throw new ValidationException($"Failed to execute script with javascript error.", new ValidationError(ex.Message)); + } + } + + private Engine CreateScriptEngine(ScriptContext context) + { + var engine = new Engine(options => options.TimeoutInterval(Timeout).Strict()); + + var contextInstance = new ObjectInstance(engine); + + if (context.Data != null) + { + contextInstance.FastAddProperty("data", new ContentDataObject(engine, context.Data), true, true, true); + } + + if (context.OldData != null) + { + contextInstance.FastAddProperty("oldData", new ContentDataObject(engine, context.OldData), true, true, true); + } + + if (context.User != null) + { + contextInstance.FastAddProperty("user", new JintUser(engine, context.User), false, true, false); + } + + if (!string.IsNullOrWhiteSpace(context.Operation)) + { + contextInstance.FastAddProperty("operation", context.Operation, false, true, false); + } + + engine.SetValue("ctx", contextInstance); + + return engine; + } + + private static void EnableDisallow(Engine engine) + { + engine.SetValue("disallow", new Action(message => + { + var exMessage = !string.IsNullOrWhiteSpace(message) ? message : "Not allowed"; + + throw new DomainForbiddenException(exMessage); + })); + } + + private static void EnableReject(Engine engine) + { + engine.SetValue("reject", new Action(message => + { + var errors = !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null; + + throw new ValidationException($"Script rejected the operation.", errors); + })); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs new file mode 100644 index 000000000..8974099d1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// JintUser.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using Jint; +using Jint.Native; +using Jint.Native.Object; +using Squidex.Infrastructure.Security; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class JintUser : ObjectInstance + { + public JintUser(Engine engine, ClaimsPrincipal principal) + : base(engine) + { + var subjectId = principal.OpenIdSubject(); + + var isClient = string.IsNullOrWhiteSpace(subjectId); + + if (!isClient) + { + FastAddProperty("id", subjectId, false, true, false); + FastAddProperty("isClient", false, false, true, false); + } + else + { + FastAddProperty("id", principal.OpenIdClientId(), false, true, false); + FastAddProperty("isClient", true, false, true, false); + } + + FastAddProperty("email", principal.OpenIdEmail(), false, true, false); + + var claimsInstance = new ObjectInstance(engine); + + foreach (var group in principal.Claims.GroupBy(x => x.Type)) + { + claimsInstance.FastAddProperty(group.Key, engine.Array.Construct(group.Select(x => new JsValue(x.Value)).ToArray()), false, true, false); + } + + FastAddProperty("claims", claimsInstance, false, true, false); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs new file mode 100644 index 000000000..fcc5a0733 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptContext.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// ScriptContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class ScriptContext + { + public ClaimsPrincipal User { get; set; } + + public Guid ContentId { get; set; } + + public NamedContentData Data { get; set; } + + public NamedContentData OldData { get; set; } + + public string Operation { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj new file mode 100644 index 000000000..ec9a21f4c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -0,0 +1,28 @@ + + + netstandard2.0 + Squidex.Domain.Apps.Core + + + full + True + + + + + + + + + + + + + + + + + + ..\..\Squidex.ruleset + + diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs new file mode 100644 index 000000000..e08089efc --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// ContentExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class ContentValidationExtensions + { + public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) + { + var validator = new ContentValidator(schema, partitionResolver, context); + + await validator.ValidateAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } + } + + public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) + { + var validator = new ContentValidator(schema, partitionResolver, context); + + await validator.ValidatePartialAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs new file mode 100644 index 000000000..d9470234f --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -0,0 +1,142 @@ +// ========================================================================== +// ContentValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +#pragma warning disable 168 + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ContentValidator + { + private readonly Schema schema; + private readonly PartitionResolver partitionResolver; + private readonly ValidationContext context; + private readonly ConcurrentBag errors = new ConcurrentBag(); + + public IReadOnlyCollection Errors + { + get { return errors; } + } + + public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(partitionResolver, nameof(partitionResolver)); + + this.schema = schema; + this.context = context; + this.partitionResolver = partitionResolver; + } + + public Task ValidatePartialAsync(NamedContentData data) + { + Guard.NotNull(data, nameof(data)); + + var tasks = new List(); + + foreach (var fieldData in data) + { + var fieldName = fieldData.Key; + + if (!schema.FieldsByName.TryGetValue(fieldData.Key, out var field)) + { + errors.AddError(" is not a known field.", fieldName); + } + else + { + tasks.Add(ValidateFieldPartialAsync(field, fieldData.Value)); + } + } + + return Task.WhenAll(tasks); + } + + private Task ValidateFieldPartialAsync(Field field, ContentFieldData fieldData) + { + var partitioning = field.Partitioning; + var partition = partitionResolver(partitioning); + + var tasks = new List(); + + foreach (var partitionValues in fieldData) + { + if (partition.TryGetItem(partitionValues.Key, out var item)) + { + tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); + } + else + { + errors.AddError($" has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field); + } + } + + return Task.WhenAll(tasks); + } + + public Task ValidateAsync(NamedContentData data) + { + Guard.NotNull(data, nameof(data)); + + ValidateUnknownFields(data); + + var tasks = new List(); + + foreach (var field in schema.FieldsByName.Values) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + + tasks.Add(ValidateFieldAsync(field, fieldData)); + } + + return Task.WhenAll(tasks); + } + + private void ValidateUnknownFields(NamedContentData data) + { + foreach (var fieldData in data) + { + if (!schema.FieldsByName.ContainsKey(fieldData.Key)) + { + errors.AddError(" is not a known field.", fieldData.Key); + } + } + } + + private Task ValidateFieldAsync(Field field, ContentFieldData fieldData) + { + var partitioning = field.Partitioning; + var partition = partitionResolver(partitioning); + + var tasks = new List(); + + foreach (var partitionValues in fieldData) + { + if (!partition.TryGetItem(partitionValues.Key, out var _)) + { + errors.AddError($" has an unsupported {partitioning.Key} value '{partitionValues.Key}'.", field); + } + } + + foreach (var item in partition) + { + var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull()); + + tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); + } + + return Task.WhenAll(tasks); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs new file mode 100644 index 000000000..b56bf1908 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// FieldExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public static class FieldExtensions + { + public static void AddError(this ConcurrentBag errors, string message, Field field, IFieldPartitionItem partitionItem = null) + { + AddError(errors, message, !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name, field.Name, partitionItem); + } + + public static void AddError(this ConcurrentBag errors, string message, string fieldName, IFieldPartitionItem partitionItem = null) + { + AddError(errors, message, fieldName, fieldName, partitionItem); + } + + public static void AddError(this ConcurrentBag errors, string message, string displayName, string fieldName, IFieldPartitionItem partitionItem = null) + { + if (partitionItem != null && partitionItem != InvariantPartitioning.Instance.Master) + { + displayName += $" ({partitionItem.Key})"; + } + + errors.Add(new ValidationError(message.Replace("", displayName), fieldName)); + } + + public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action addError) + { + try + { + var typedValue = value.IsNull() ? null : JsonValueConverter.ConvertValue(field, value); + + foreach (var validator in ValidatorsFactory.CreateValidators(field)) + { + await validator.ValidateAsync(typedValue, context, addError); + } + } + catch + { + addError(" is not a valid value."); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs new file mode 100644 index 000000000..00f4f3e9d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -0,0 +1,113 @@ +// ========================================================================== +// JsonValueConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using NodaTime.Text; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class JsonValueConverter : IFieldVisitor + { + public JToken Value { get; } + + private JsonValueConverter(JToken value) + { + this.Value = value; + } + + public static object ConvertValue(Field field, JToken json) + { + return field.Accept(new JsonValueConverter(json)); + } + + public object Visit(AssetsField field) + { + return Value.ToObject>(); + } + + public object Visit(BooleanField field) + { + return (bool?)Value; + } + + public object Visit(DateTimeField field) + { + if (Value.Type == JTokenType.String) + { + var parseResult = InstantPattern.General.Parse(Value.ToString()); + + if (!parseResult.Success) + { + throw parseResult.Exception; + } + + return parseResult.Value; + } + + throw new InvalidCastException("Invalid json type, expected string."); + } + + public object Visit(GeolocationField field) + { + var geolocation = (JObject)Value; + + foreach (var property in geolocation.Properties()) + { + if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) && + !string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + } + } + + var lat = (double)geolocation["latitude"]; + var lon = (double)geolocation["longitude"]; + + if (!lat.IsBetween(-90, 90)) + { + throw new InvalidCastException("Latitude must be between -90 and 90."); + } + + if (!lon.IsBetween(-180, 180)) + { + throw new InvalidCastException("Longitude must be between -180 and 180."); + } + + return Value; + } + + public object Visit(JsonField field) + { + return Value; + } + + public object Visit(NumberField field) + { + return (double?)Value; + } + + public object Visit(ReferencesField field) + { + return Value.ToObject>(); + } + + public object Visit(StringField field) + { + return Value.ToString(); + } + + public object Visit(TagsField field) + { + return Value.ToObject>(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs new file mode 100644 index 000000000..2d6c9925d --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// ValidationContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ValidationContext + { + private readonly Func, Guid, Task>> checkContent; + private readonly Func, Task>> checkAsset; + + public bool IsOptional { get; } + + public ValidationContext( + Func, Guid, Task>> checkContent, + Func, Task>> checkAsset) + : this(checkContent, checkAsset, false) + { + } + + private ValidationContext( + Func, Guid, Task>> checkContent, + Func, Task>> checkAsset, + bool isOptional) + { + Guard.NotNull(checkAsset, nameof(checkAsset)); + Guard.NotNull(checkContent, nameof(checkAsset)); + + this.checkContent = checkContent; + this.checkAsset = checkAsset; + + IsOptional = isOptional; + } + + public ValidationContext Optional(bool isOptional) + { + return isOptional == IsOptional ? this : new ValidationContext(checkContent, checkAsset, isOptional); + } + + public Task> GetInvalidContentIdsAsync(IEnumerable contentIds, Guid schemaId) + { + return checkContent(contentIds, schemaId); + } + + public Task> GetInvalidAssetIdsAsync(IEnumerable assetId) + { + return checkAsset(assetId); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs new file mode 100644 index 000000000..e5fd7706b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AllowedValuesValidator.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// AllowedValuesValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AllowedValuesValidator : IValidator + { + private readonly T[] allowedValues; + + public AllowedValuesValidator(params T[] allowedValues) + { + Guard.NotNull(allowedValues, nameof(allowedValues)); + + this.allowedValues = allowedValues; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value == null) + { + return TaskHelper.Done; + } + + var typedValue = (T)value; + + if (!allowedValues.Contains(typedValue)) + { + addError(" is not an allowed value."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs new file mode 100644 index 000000000..e7c504577 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// AssetsValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class AssetsValidator : IValidator + { + public async Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is ICollection assetIds) + { + var invalidIds = await context.GetInvalidAssetIdsAsync(assetIds); + + foreach (var invalidId in invalidIds) + { + addError($" contains invalid asset '{invalidId}'."); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs new file mode 100644 index 000000000..750acad7c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionItemValidator.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// CollectionItemValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionItemValidator : IValidator + { + private readonly IValidator[] itemValidators; + + public CollectionItemValidator(params IValidator[] itemValidators) + { + Guard.NotNull(itemValidators, nameof(itemValidators)); + Guard.NotEmpty(itemValidators, nameof(itemValidators)); + + this.itemValidators = itemValidators; + } + + public async Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is ICollection items) + { + var innerContext = context.Optional(false); + + var index = 1; + + foreach (var item in items) + { + foreach (var itemValidator in itemValidators) + { + await itemValidator.ValidateAsync(item, innerContext, e => addError(e.Replace("", $" item #{index}"))); + } + + index++; + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs new file mode 100644 index 000000000..6f24890c2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// CollectionValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class CollectionValidator : IValidator + { + private readonly bool isRequired; + private readonly int? minItems; + private readonly int? maxItems; + + public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) + { + this.isRequired = isRequired; + this.minItems = minItems; + this.maxItems = maxItems; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (!(value is ICollection items) || items.Count == 0) + { + if (isRequired && !context.IsOptional) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + + if (minItems.HasValue && items.Count < minItems.Value) + { + addError($" must have at least {minItems} item(s)."); + } + + if (maxItems.HasValue && items.Count > maxItems.Value) + { + addError($" must have not more than {maxItems} item(s)."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs new file mode 100644 index 000000000..597d01d09 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/IValidator.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// IValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public interface IValidator + { + Task ValidateAsync(object value, ValidationContext context, Action addError); + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs new file mode 100644 index 000000000..927156734 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// PatternValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class PatternValidator : IValidator + { + private readonly Regex regex; + private readonly string errorMessage; + + public PatternValidator(string pattern, string errorMessage = null) + { + this.errorMessage = errorMessage; + + regex = new Regex("^" + pattern + "$"); + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is string stringValue) + { + if (!string.IsNullOrEmpty(stringValue) && !regex.IsMatch(stringValue)) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + { + addError(" is not valid."); + } + else + { + addError(errorMessage); + } + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs new file mode 100644 index 000000000..141286630 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RangeValidator.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// RangeValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class RangeValidator : IValidator where T : struct, IComparable + { + private readonly T? min; + private readonly T? max; + + public RangeValidator(T? min, T? max) + { + if (min.HasValue && max.HasValue && min.Value.CompareTo(max.Value) >= 0) + { + throw new ArgumentException("Min value must be greater than max value.", nameof(min)); + } + + this.min = min; + this.max = max; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value == null) + { + return TaskHelper.Done; + } + + var typedValue = (T)value; + + if (min.HasValue && typedValue.CompareTo(min.Value) < 0) + { + addError($" must be greater or equals than '{min}'."); + } + + if (max.HasValue && typedValue.CompareTo(max.Value) > 0) + { + addError($" must be less or equals than '{max}'."); + } + + return TaskHelper.Done; + } + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs new file mode 100644 index 000000000..081e54835 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// ReferencesValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class ReferencesValidator : IValidator + { + private readonly Guid schemaId; + + public ReferencesValidator(Guid schemaId) + { + this.schemaId = schemaId; + } + + public async Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is ICollection contentIds) + { + var invalidIds = await context.GetInvalidContentIdsAsync(contentIds, schemaId); + + foreach (var invalidId in invalidIds) + { + addError($" contains invalid reference '{invalidId}'."); + } + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs new file mode 100644 index 000000000..67f118e49 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredStringValidator.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// RequiredStringValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredStringValidator : IValidator + { + private readonly bool validateEmptyStrings; + + public RequiredStringValidator(bool validateEmptyStrings = false) + { + this.validateEmptyStrings = validateEmptyStrings; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (context.IsOptional || (value != null && !(value is string))) + { + return TaskHelper.Done; + } + + var valueAsString = (string)value; + + if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString))) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs new file mode 100644 index 000000000..2c5f51269 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/RequiredValidator.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// RequiredValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class RequiredValidator : IValidator + { + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value == null && !context.IsOptional) + { + addError(" is required."); + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs new file mode 100644 index 000000000..0a742ba49 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// StringLengthValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public class StringLengthValidator : IValidator + { + private readonly int? minLength; + private readonly int? maxLength; + + public StringLengthValidator(int? minLength, int? maxLength) + { + if (minLength.HasValue && maxLength.HasValue && minLength.Value >= maxLength.Value) + { + throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); + } + + this.minLength = minLength; + this.maxLength = maxLength; + } + + public Task ValidateAsync(object value, ValidationContext context, Action addError) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (minLength.HasValue && stringValue.Length < minLength.Value) + { + addError($" must have more than '{minLength}' characters."); + } + + if (maxLength.HasValue && stringValue.Length > maxLength.Value) + { + addError($" must have less than '{maxLength}' characters."); + } + } + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs new file mode 100644 index 000000000..735b079ad --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs @@ -0,0 +1,145 @@ +// ========================================================================== +// ValidatorsFactory.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.ValidateContent +{ + public sealed class ValidatorsFactory : IFieldPropertiesVisitor> + { + private static readonly ValidatorsFactory Instance = new ValidatorsFactory(); + + private ValidatorsFactory() + { + } + + public static IEnumerable CreateValidators(Field field) + { + Guard.NotNull(field, nameof(field)); + + return field.RawProperties.Accept(Instance); + } + + public IEnumerable Visit(AssetsFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + yield return new AssetsValidator(); + } + + public IEnumerable Visit(BooleanFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(DateTimeFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + { + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } + } + + public IEnumerable Visit(GeolocationFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(JsonFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public IEnumerable Visit(NumberFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredValidator(); + } + + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) + { + yield return new RangeValidator(properties.MinValue, properties.MaxValue); + } + + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues.ToArray()); + } + } + + public IEnumerable Visit(ReferencesFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + if (properties.SchemaId != Guid.Empty) + { + yield return new ReferencesValidator(properties.SchemaId); + } + } + + public IEnumerable Visit(StringFieldProperties properties) + { + if (properties.IsRequired) + { + yield return new RequiredStringValidator(); + } + + if (properties.MinLength.HasValue || properties.MaxLength.HasValue) + { + yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); + } + + if (!string.IsNullOrWhiteSpace(properties.Pattern)) + { + yield return new PatternValidator(properties.Pattern, properties.PatternMessage); + } + + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues.ToArray()); + } + } + + public IEnumerable Visit(TagsFieldProperties properties) + { + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) + { + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); + } + + yield return new CollectionItemValidator(new RequiredStringValidator()); + } + } +} diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index 72df850d5..9a3b56d23 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -26,7 +26,7 @@ if ($all -Or $infrastructure) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Infrastructure.Tests\Squidex.Infrastructure.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Infrastructure*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Infrastructure.xml" ` -oldStyle @@ -37,7 +37,7 @@ if ($all -Or $appsCore) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Core.Tests\Squidex.Domain.Apps.Core.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Core*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Core.xml" ` -oldStyle @@ -48,7 +48,7 @@ if ($all -Or $appsRead) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Read.Tests\Squidex.Domain.Apps.Read.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Read*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Read.xml" ` -oldStyle @@ -59,7 +59,7 @@ if ($all -Or $appsWrite) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Apps.Write.Tests\Squidex.Domain.Apps.Write.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Apps.Write*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Write.xml" ` -oldStyle @@ -70,7 +70,7 @@ if ($all -Or $users) { -register:user ` -target:"C:\Program Files\dotnet\dotnet.exe" ` -targetargs:"test $folderWorking\Squidex.Domain.Users.Tests\Squidex.Domain.Users.Tests.csproj" ` - -filter:"+[Squidex*]*" ` + -filter:"+[Squidex.Domain.Users*]*" ` -skipautoprops ` -output:"$folderWorking\$folderReports\Users.xml" ` -oldStyle diff --git a/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs deleted file mode 100644 index 6a147aeb1..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/ContentEnrichmentTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// ContentEnrichmentTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using NodaTime.Text; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core -{ - public class ContentEnrichmentTests - { - private static readonly Instant Now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - private readonly Schema schema = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new StringField(2, "my-string", Partitioning.Language, - new StringFieldProperties { DefaultValue = "en-string" })) - .AddField(new NumberField(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties { DefaultValue = 123 })) - .AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties { DefaultValue = true })) - .AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { DefaultValue = Now })) - .AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(8, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())); - - [Fact] - private void Should_enrich_with_default_values() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "de-string")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal(456, (int)data["my-number"]["iv"]); - - Assert.Equal("de-string", (string)data["my-string"]["de"]); - Assert.Equal("en-string", (string)data["my-string"]["en"]); - - Assert.Equal(Now, InstantPattern.General.Parse((string)data["my-datetime"]["iv"]).Value); - - Assert.True((bool)data["my-boolean"]["iv"]); - } - - [Fact] - private void Should_also_enrich_with_default_values_when_string_is_empty() - { - var data = - new NamedContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", string.Empty)) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - data.Enrich(schema, languagesConfig.ToResolver()); - - Assert.Equal("en-string", (string)data["my-string"]["de"]); - Assert.Equal("en-string", (string)data["my-string"]["en"]); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs similarity index 95% rename from tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs index 743d541a6..89927742e 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Apps/RoleExtensionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleExtensionTests.cs @@ -7,9 +7,10 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Core.Apps; using Xunit; -namespace Squidex.Domain.Apps.Core.Apps +namespace Squidex.Domain.Apps.Core.Model.Apps { public class RoleExtensionTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs new file mode 100644 index 000000000..cb4b36c77 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -0,0 +1,208 @@ +// ========================================================================== +// ContentDataTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Model.Contents +{ + public class ContentDataTests + { + [Fact] + public void Should_remove_null_values_from_name_model_when_cleaning() + { + var input = + new NamedContentData() + .AddField("field1", null) + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2) + .AddValue("it", null)); + + var actual = input.ToCleaned(); + + var expected = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2)); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_remove_null_values_from_id_model_when_cleaning() + { + var input = + new IdContentData() + .AddField(1, null) + .AddField(2, + new ContentFieldData() + .AddValue("en", 2) + .AddValue("it", null)); + + var actual = input.ToCleaned(); + + var expected = + new IdContentData() + .AddField(2, + new ContentFieldData() + .AddValue("en", 2)); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_merge_two_name_models() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", 2)); + + var rhs = + new NamedContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 4)); + + var expected = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", 2) + .AddValue("en", 3)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_merge_two_id_models() + { + var lhs = + new IdContentData() + .AddField(1, + new ContentFieldData() + .AddValue("iv", 1)) + .AddField(2, + new ContentFieldData() + .AddValue("de", 2)); + + var rhs = + new IdContentData() + .AddField(2, + new ContentFieldData() + .AddValue("en", 3)) + .AddField(3, + new ContentFieldData() + .AddValue("iv", 4)); + + var expected = + new IdContentData() + .AddField(1, + new ContentFieldData() + .AddValue("iv", 1)) + .AddField(2, + new ContentFieldData() + .AddValue("de", 2) + .AddValue("en", 3)) + .AddField(3, + new ContentFieldData() + .AddValue("iv", 4)); + + var actual = lhs.MergeInto(rhs); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_be_equal_when_data_have_same_structure() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var rhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } + + [Fact] + public void Should_not_be_equal_when_data_have_not_same_structure() + { + var lhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("field2", + new ContentFieldData() + .AddValue("iv", 2)); + + var rhs = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("en", 2)) + .AddField("field3", + new ContentFieldData() + .AddValue("iv", 2)); + + Assert.False(lhs.Equals(rhs)); + Assert.False(lhs.Equals((object)rhs)); + Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); + } + + [Fact] + public void Should_be_equal_fields_when_they_have_same_value() + { + var lhs = + new ContentFieldData() + .AddValue("iv", 2); + + var rhs = + new ContentFieldData() + .AddValue("iv", 2); + + Assert.True(lhs.Equals(rhs)); + Assert.True(lhs.Equals((object)rhs)); + Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs index bf7060ff4..5600a3122 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Contents/StatusFlowTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/StatusFlowTests.cs @@ -6,9 +6,10 @@ // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; using Xunit; -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Model.Contents { public class StatusFlowTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs similarity index 97% rename from tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs index a39b6f13c..0a5c4f8e7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/InvariantPartitionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/InvariantPartitionTests.cs @@ -13,7 +13,7 @@ using Xunit; #pragma warning disable xUnit2013 // Do not use equality check to check for collection size. -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public sealed class InvariantPartitionTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs similarity index 58% rename from tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs index 7584f61ab..d801a9810 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -13,14 +14,14 @@ using FluentAssertions; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public class LanguagesConfigTests { [Fact] public void Should_create_initial_config() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_create_initial_config_with_multiple_languages() { - var config = LanguagesConfig.Create(Language.DE, Language.EN, Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.EN, Language.IT); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -59,7 +60,7 @@ namespace Squidex.Domain.Apps.Core new LanguageConfig(Language.EN), new LanguageConfig(Language.IT) }; - var config = LanguagesConfig.Create(configs); + var config = LanguagesConfig.Build(configs); config.OfType().ToList().ShouldBeEquivalentTo(configs); @@ -69,7 +70,9 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_add_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.IT)); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -77,28 +80,35 @@ namespace Squidex.Domain.Apps.Core new LanguageConfig(Language.DE), new LanguageConfig(Language.IT) }); + + Assert.True(config.TryGetConfig(Language.IT, out var _)); + Assert.True(config.Contains(Language.IT)); } [Fact] public void Should_make_first_language_to_master() { - var config = LanguagesConfig.Empty.Add(Language.IT); + var config = LanguagesConfig.Build(Language.IT); Assert.Equal(Language.IT, config.Master.Language); } [Fact] - public void Should_throw_exception_if_language_to_add_already_exists() + public void Should_not_throw_exception_if_language_to_add_already_exists() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Add(Language.DE)); + config.Set(new LanguageConfig(Language.DE)); } [Fact] public void Should_make_master_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).MakeMaster(Language.IT); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.UK)); + config.Set(new LanguageConfig(Language.IT)); + config.MakeMaster(Language.IT); Assert.Equal(Language.IT, config.Master.Language); } @@ -106,15 +116,15 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_throw_exception_if_language_to_make_master_is_not_found() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.MakeMaster(Language.EN)); + Assert.Throws(() => config.MakeMaster(Language.EN)); } [Fact] public void Should_not_throw_exception_if_language_is_already_master_language() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); config.MakeMaster(Language.DE); } @@ -122,7 +132,9 @@ namespace Squidex.Domain.Apps.Core [Fact] public void Should_remove_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Add(Language.RU).Remove(Language.IT); + var config = LanguagesConfig.Build(Language.DE, Language.IT, Language.RU); + + config.Remove(Language.IT); config.ToList().ShouldBeEquivalentTo( new List @@ -136,41 +148,44 @@ namespace Squidex.Domain.Apps.Core public void Should_remove_fallbacks_when_removing_language() { var config = - LanguagesConfig.Create(Language.DE) - .Add(Language.IT) - .Add(Language.RU) - .Update(Language.DE, false, false, new[] { Language.RU, Language.IT }) - .Update(Language.RU, false, false, new[] { Language.DE, Language.IT }) - .Remove(Language.IT); + LanguagesConfig.Build( + new LanguageConfig(Language.DE), + new LanguageConfig(Language.IT, false, Language.RU, Language.IT), + new LanguageConfig(Language.RU, false, Language.DE, Language.IT)); + + config.Remove(Language.IT); config.OfType().ToList().ShouldBeEquivalentTo( new List { - new LanguageConfig(Language.DE, false, Language.RU), + new LanguageConfig(Language.DE), new LanguageConfig(Language.RU, false, Language.DE) }); } [Fact] - public void Should_throw_exception_if_language_to_remove_is_not_found() + public void Should_not_throw_exception_if_language_to_remove_is_not_found() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Remove(Language.EN)); + config.Remove(Language.EN); } [Fact] public void Should_throw_exception_if_language_to_remove_is_master_language() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Remove(Language.DE)); + Assert.Throws(() => config.Remove(Language.DE)); } [Fact] public void Should_update_language() { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, false, new[] { Language.DE }); + var config = LanguagesConfig.Build(Language.DE); + + config.Set(new LanguageConfig(Language.IT)); + config.Set(new LanguageConfig(Language.IT, true, Language.DE)); config.OfType().ToList().ShouldBeEquivalentTo( new List @@ -180,52 +195,28 @@ namespace Squidex.Domain.Apps.Core }); } - [Fact] - public void Should_also_set_make_master_when_updating_language() - { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, false, true, null); - - Assert.Equal(Language.IT, config.Master.Language); - } - - [Fact] - public void Should_throw_exception_if_language_to_update_is_not_found() - { - var config = LanguagesConfig.Create(Language.DE); - - Assert.Throws(() => config.Update(Language.EN, true, false, null)); - } - [Fact] public void Should_throw_exception_if_fallback_language_is_invalid() { - var config = LanguagesConfig.Create(Language.DE); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Update(Language.DE, false, false, new[] { Language.EN })); + Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, false, Language.EN))); } [Fact] public void Should_throw_exception_if_language_to_make_optional_is_master_language() { - var config = LanguagesConfig.Create(Language.DE); - - Assert.Throws(() => config.Update(Language.DE, true, false, null)); - } - - [Fact] - public void Should_throw_exception_if_language_to_make_optional_must_be_set_to_master() - { - var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); + var config = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => config.Update(Language.DE, true, true, null)); + Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, true))); } [Fact] public void Should_provide_enumerators() { - var config = LanguagesConfig.Create(); + var config = LanguagesConfig.Build(Language.DE); - Assert.Empty(config); + Assert.NotEmpty(config); Assert.NotNull(((IEnumerable)config).GetEnumerator()); Assert.NotNull(((IEnumerable)config).GetEnumerator()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs index 41fbb3c00..10ed1df6f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/PartitioningTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/PartitioningTests.cs @@ -8,7 +8,7 @@ using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Model { public sealed class PartitioningTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs new file mode 100644 index 000000000..8195d2beb --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/FieldRegistryTests.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// FieldRegistryTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class FieldRegistryTests + { + private readonly FieldRegistry sut = new FieldRegistry(new TypeNameRegistry()); + + private sealed class InvalidProperties : FieldProperties + { + public override T Accept(IFieldPropertiesVisitor visitor) + { + return default(T); + } + } + + [Fact] + public void Should_throw_exception_if_creating_field_and_field_is_not_registered() + { + Assert.Throws(() => sut.CreateField(1, "name", Partitioning.Invariant, new InvalidProperties())); + } + + [Theory] + [InlineData( + typeof(AssetsFieldProperties), + typeof(AssetsField))] + [InlineData( + typeof(BooleanFieldProperties), + typeof(BooleanField))] + [InlineData( + typeof(DateTimeFieldProperties), + typeof(DateTimeField))] + [InlineData( + typeof(GeolocationFieldProperties), + typeof(GeolocationField))] + [InlineData( + typeof(JsonFieldProperties), + typeof(JsonField))] + [InlineData( + typeof(NumberFieldProperties), + typeof(NumberField))] + [InlineData( + typeof(ReferencesFieldProperties), + typeof(ReferencesField))] + [InlineData( + typeof(StringFieldProperties), + typeof(StringField))] + [InlineData( + typeof(TagsFieldProperties), + typeof(TagsField))] + public void Should_create_field_by_properties(Type propertyType, Type fieldType) + { + var properties = (FieldProperties)Activator.CreateInstance(propertyType); + + var field = sut.CreateField(1, "name", Partitioning.Invariant, properties); + + Assert.Equal(properties, field.RawProperties); + Assert.Equal(fieldType, field.GetType()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs new file mode 100644 index 000000000..941c85d5e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// JsonSerializerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas.Json +{ + public class JsonSerializerTests + { + private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); + private readonly JsonSerializer serializer; + private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + + public JsonSerializerTests() + { + serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); + + serializerSettings.ContractResolver = new ConverterContractResolver( + new InstantConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new RefTokenConverter(), + new SchemaConverter(new FieldRegistry(typeNameRegistry)), + new StringEnumConverter()); + + serializerSettings.TypeNameHandling = TypeNameHandling.Auto; + + serializer = JsonSerializer.Create(serializerSettings); + } + + [Fact] + public void Should_serialize_and_deserialize_schema() + { + var schemaSource = TestData.MixedSchema(); + var schemaTarget = JToken.FromObject(schemaSource, serializer).ToObject(serializer); + + schemaTarget.ShouldBeEquivalentTo(schemaSource); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs new file mode 100644 index 000000000..8d5fbebfa --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -0,0 +1,217 @@ +// ========================================================================== +// SchemaTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class SchemaTests + { + private readonly Schema sut = new Schema("my-schema"); + + [Fact] + public void Should_instantiate_schema() + { + Assert.Equal("my-schema", sut.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_schema_with_invalid_name() + { + Assert.Throws(() => new Schema(string.Empty)); + } + + [Fact] + public void Should_update_schema() + { + var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; + + sut.Update(properties); + + Assert.Equal(properties, sut.Properties); + } + + [Fact] + public void Should_add_field() + { + var field = AddNumberField(1); + + Assert.Equal(field, sut.FieldsById[1]); + } + + [Fact] + public void Should_throw_exception_if_adding_field_with_name_that_already_exists() + { + AddNumberField(1); + + Assert.Throws(() => sut.AddField(new NumberField(2, "my-field-1", Partitioning.Invariant))); + } + + [Fact] + public void Should_throw_exception_if_adding_field_with_id_that_already_exists() + { + AddNumberField(1); + + Assert.Throws(() => sut.AddField(new NumberField(1, "my-field-2", Partitioning.Invariant))); + } + + [Fact] + public void Should_hide_field() + { + AddNumberField(1); + + sut.FieldsById[1].Hide(); + sut.FieldsById[1].Hide(); + + Assert.True(sut.FieldsById[1].IsHidden); + } + + [Fact] + public void Should_show_field() + { + AddNumberField(1); + + sut.FieldsById[1].Hide(); + sut.FieldsById[1].Show(); + sut.FieldsById[1].Show(); + + Assert.False(sut.FieldsById[1].IsHidden); + } + + [Fact] + public void Should_disable_field() + { + AddNumberField(1); + + sut.FieldsById[1].Disable(); + sut.FieldsById[1].Disable(); + + Assert.True(sut.FieldsById[1].IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + AddNumberField(1); + + sut.FieldsById[1].Disable(); + sut.FieldsById[1].Enable(); + sut.FieldsById[1].Enable(); + + Assert.False(sut.FieldsById[1].IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + AddNumberField(1); + + sut.FieldsById[1].Lock(); + + Assert.True(sut.FieldsById[1].IsLocked); + } + + [Fact] + public void Should_do_nothing_if_field_to_delete_not_found() + { + AddNumberField(1); + + sut.DeleteField(2); + + Assert.Equal(1, sut.FieldsById.Count); + } + + [Fact] + public void Should_delete_field() + { + AddNumberField(1); + + sut.DeleteField(1); + + Assert.Empty(sut.FieldsById); + } + + [Fact] + public void Should_update_field() + { + AddNumberField(1); + + sut.FieldsById[1].Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + AddNumberField(1); + + Assert.Throws(() => sut.FieldsById[1].Update(new StringFieldProperties())); + } + + [Fact] + public void Should_publish_schema() + { + sut.Publish(); + + Assert.True(sut.IsPublished); + } + + [Fact] + public void Should_unpublish_schema() + { + sut.Publish(); + sut.Unpublish(); + + Assert.False(sut.IsPublished); + } + + [Fact] + public void Should_reorder_fields() + { + var field1 = AddNumberField(1); + var field2 = AddNumberField(2); + var field3 = AddNumberField(3); + + sut.ReorderFields(new List { 3, 2, 1 }); + + Assert.Equal(new List { field3, field2, field1 }, sut.Fields.ToList()); + } + + [Fact] + public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() + { + AddNumberField(1); + AddNumberField(2); + + Assert.Throws(() => sut.ReorderFields(new List { 1 })); + } + + [Fact] + public void Should_throw_exception_if_field_to_reorder_does_not_exist() + { + AddNumberField(1); + AddNumberField(2); + + Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); + } + + private NumberField AddNumberField(int id) + { + var field = new NumberField(id, $"my-field-{id}", Partitioning.Invariant); + + sut.AddField(field); + + return field; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs similarity index 66% rename from tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index 0824c91d3..a6c436d1a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -8,27 +8,36 @@ using System; using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Xunit; #pragma warning disable xUnit2013 // Do not use equality check to check for collection size. -namespace Squidex.Domain.Apps.Core.Contents +namespace Squidex.Domain.Apps.Core.Operations.ConvertContent { - public class ContentDataTests + public class ContentConversionTests { - private readonly Schema schema = - Schema.Create("schema", new SchemaProperties()) - .AddField(new NumberField(1, "field1", Partitioning.Language)) - .AddField(new NumberField(2, "field2", Partitioning.Invariant)) - .AddField(new NumberField(3, "field3", Partitioning.Invariant).Hide()) - .AddField(new AssetsField(5, "assets1", Partitioning.Invariant)) - .AddField(new AssetsField(6, "assets2", Partitioning.Invariant)) - .AddField(new JsonField(4, "json", Partitioning.Language)); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE); + private readonly Schema schema = new Schema("my-schema"); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + public ContentConversionTests() + { + schema.AddField(new NumberField(1, "field1", Partitioning.Language)); + schema.AddField(new NumberField(2, "field2", Partitioning.Invariant)); + schema.AddField(new NumberField(3, "field3", Partitioning.Invariant)); + + schema.AddField(new AssetsField(5, "assets1", Partitioning.Invariant)); + schema.AddField(new AssetsField(6, "assets2", Partitioning.Invariant)); + + schema.AddField(new JsonField(4, "json", Partitioning.Language)); + + schema.FieldsById[3].Hide(); + } [Fact] public void Should_convert_to_id_model() @@ -342,8 +351,9 @@ namespace Squidex.Domain.Apps.Core.Contents .AddValue("it", 7)); var fallbackConfig = - LanguagesConfig.Create(Language.DE).Add(Language.EN) - .Update(Language.DE, false, false, new[] { Language.EN }); + LanguagesConfig.Build( + new LanguageConfig(Language.EN), + new LanguageConfig(Language.DE, false, new[] { Language.EN })); var output = (Dictionary)data.ToLanguageModel(fallbackConfig, new List { Language.DE }); @@ -389,173 +399,6 @@ namespace Squidex.Domain.Apps.Core.Contents Assert.True(expected.EqualsDictionary(output)); } - [Fact] - public void Should_merge_two_name_models() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", 2)); - - var rhs = - new NamedContentData() - .AddField("field2", - new ContentFieldData() - .AddValue("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 4)); - - var expected = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 1)) - .AddField("field2", - new ContentFieldData() - .AddValue("de", 2) - .AddValue("en", 3)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 4)); - - var actual = lhs.MergeInto(rhs); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_merge_two_id_models() - { - var lhs = - new IdContentData() - .AddField(1, - new ContentFieldData() - .AddValue("iv", 1)) - .AddField(2, - new ContentFieldData() - .AddValue("de", 2)); - - var rhs = - new IdContentData() - .AddField(2, - new ContentFieldData() - .AddValue("en", 3)) - .AddField(3, - new ContentFieldData() - .AddValue("iv", 4)); - - var expected = - new IdContentData() - .AddField(1, - new ContentFieldData() - .AddValue("iv", 1)) - .AddField(2, - new ContentFieldData() - .AddValue("de", 2) - .AddValue("en", 3)) - .AddField(3, - new ContentFieldData() - .AddValue("iv", 4)); - - var actual = lhs.MergeInto(rhs); - - Assert.Equal(expected, actual); - } - - [Fact] - public void Should_be_equal_when_data_have_same_structure() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - var rhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - Assert.True(lhs.Equals(rhs)); - Assert.True(lhs.Equals((object)rhs)); - Assert.Equal(lhs.GetHashCode(), rhs.GetHashCode()); - } - - [Fact] - public void Should_not_be_equal_when_data_have_not_same_structure() - { - var lhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("iv", 2)) - .AddField("field2", - new ContentFieldData() - .AddValue("iv", 2)); - - var rhs = - new NamedContentData() - .AddField("field1", - new ContentFieldData() - .AddValue("en", 2)) - .AddField("field3", - new ContentFieldData() - .AddValue("iv", 2)); - - Assert.False(lhs.Equals(rhs)); - Assert.False(lhs.Equals((object)rhs)); - Assert.NotEqual(lhs.GetHashCode(), rhs.GetHashCode()); - } - - [Fact] - public void Should_remove_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new NamedContentData() - .AddField("assets1", - new ContentFieldData() - .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); - - var ids = input.GetReferencedIds(schema).ToArray(); - - Assert.Equal(new[] { id1, id2 }, ids); - } - - [Fact] - public void Should_cleanup_deleted_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var input = - new IdContentData() - .AddField(5, - new ContentFieldData() - .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); - - var actual = input.ToCleanedReferences(schema, new HashSet(new[] { id2 })); - - var cleanedValue = (JArray)actual[5]["iv"]; - - Assert.Equal(1, cleanedValue.Count); - Assert.Equal(id1.ToString(), cleanedValue[0]); - } - [Fact] public void Should_be_equal_fields_when_they_have_same_value() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs new file mode 100644 index 000000000..5f2b899d0 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -0,0 +1,202 @@ +// ========================================================================== +// ContentEnrichmentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions + +namespace Squidex.Domain.Apps.Core.Operations.EnrichContent +{ + public class ContentEnrichmentTests + { + private static readonly Instant Now = SystemClock.Instance.GetCurrentInstant(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + private readonly Schema schema; + + public ContentEnrichmentTests() + { + schema = new Schema("my-schema"); + + schema.AddField(new StringField(1, "my-string", Partitioning.Language, + new StringFieldProperties { DefaultValue = "en-string" })); + + schema.AddField(new NumberField(2, "my-number", Partitioning.Invariant, + new NumberFieldProperties())); + + schema.AddField(new DateTimeField(3, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = Now })); + + schema.AddField(new BooleanField(4, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true })); + } + + [Fact] + private void Should_enrich_with_default_values() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "de-string")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal(456, (int)data["my-number"]["iv"]); + + Assert.Equal("de-string", (string)data["my-string"]["de"]); + Assert.Equal("en-string", (string)data["my-string"]["en"]); + + Assert.Equal(Now.ToString(), (string)data["my-datetime"]["iv"]); + + Assert.True((bool)data["my-boolean"]["iv"]); + } + + [Fact] + private void Should_also_enrich_with_default_values_when_string_is_empty() + { + var data = + new NamedContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", string.Empty)) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languagesConfig.ToResolver()); + + Assert.Equal("en-string", (string)data["my-string"]["de"]); + Assert.Equal("en-string", (string)data["my-string"]["en"]); + } + + [Fact] + public void Should_get_default_value_from_assets_field() + { + var field = + new AssetsField(1, "1", Partitioning.Invariant, + new AssetsFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_boolean_field() + { + var field = + new BooleanField(1, "1", Partitioning.Invariant, + new BooleanFieldProperties { DefaultValue = true }); + + Assert.Equal(true, DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { DefaultValue = FutureDays(15) }); + + Assert.Equal(FutureDays(15).ToString(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_today() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }); + + Assert.Equal(Now.ToString().Substring(10), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_datetime_field_when_set_to_now() + { + var field = + new DateTimeField(1, "1", Partitioning.Invariant, + new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }); + + Assert.Equal(Now.ToString(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_json_field() + { + var field = + new JsonField(1, "1", Partitioning.Invariant, + new JsonFieldProperties()); + + Assert.Equal(new JObject(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_geolocation_field() + { + var field = + new GeolocationField(1, "1", Partitioning.Invariant, + new GeolocationFieldProperties()); + + Assert.Equal(JValue.CreateNull(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_number_field() + { + var field = + new NumberField(1, "1", Partitioning.Invariant, + new NumberFieldProperties { DefaultValue = 12 }); + + Assert.Equal(12, DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_references_field() + { + var field = + new ReferencesField(1, "1", Partitioning.Invariant, + new ReferencesFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_string_field() + { + var field = + new StringField(1, "1", Partitioning.Invariant, + new StringFieldProperties { DefaultValue = "default" }); + + Assert.Equal("default", DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + [Fact] + public void Should_get_default_value_from_tags_field() + { + var field = + new TagsField(1, "1", Partitioning.Invariant, + new TagsFieldProperties()); + + Assert.Equal(new JArray(), DefaultValueFactory.CreateDefaultValue(field, Now)); + } + + private static Instant FutureDays(int days) + { + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs new file mode 100644 index 000000000..7f4af1cd6 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -0,0 +1,243 @@ +// ========================================================================== +// ReferenceExtractionTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable xUnit2013 // Do not use equality check to check for collection size. + +namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds +{ + public class ReferenceExtractionTests + { + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Schema schema = new Schema("my-schema"); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); + + public ReferenceExtractionTests() + { + schema.AddField(new NumberField(1, "field1", Partitioning.Language)); + schema.AddField(new NumberField(2, "field2", Partitioning.Invariant)); + schema.AddField(new NumberField(3, "field3", Partitioning.Invariant)); + + schema.AddField(new AssetsField(5, "assets1", Partitioning.Invariant)); + schema.AddField(new AssetsField(6, "assets2", Partitioning.Invariant)); + + schema.AddField(new JsonField(4, "json", Partitioning.Language)); + + schema.FieldsById[3].Hide(); + } + + [Fact] + public void Should_remove_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); + + var ids = input.GetReferencedIds(schema).ToArray(); + + Assert.Equal(new[] { id1, id2 }, ids); + } + + [Fact] + public void Should_cleanup_deleted_ids() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var input = + new IdContentData() + .AddField(5, + new ContentFieldData() + .AddValue("iv", new JArray(id1.ToString(), id2.ToString()))); + + var actual = input.ToCleanedReferences(schema, new HashSet(new[] { id2 })); + + var cleanedValue = (JArray)actual[5]["iv"]; + + Assert.Equal(1, cleanedValue.Count); + Assert.Equal(id1.ToString(), cleanedValue[0]); + } + + [Fact] + public void Should_return_ids_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2 }, result); + } + + [Fact] + public void Should_empty_list_from_assets_field_for_referenced_ids_when_null() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences(null).ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_empty_list_from_assets_field_for_referenced_ids_when_other_type() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.ExtractReferences("invalid").ToArray(); + + Assert.Empty(result); + } + + [Fact] + public void Should_return_null_from_assets_field_when_removing_references_from_null_array() + { + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(null, null); + + Assert.Null(result); + } + + [Fact] + public void Should_remove_deleted_references_from_assets_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); + + Assert.Equal(CreateValue(id1), result); + } + + [Fact] + public void Should_return_same_token_from_assets_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + var result = sut.CleanReferences(token, new HashSet(new[] { Guid.NewGuid() })); + + Assert.Same(token, result); + } + + [Fact] + public void Should_return_ids_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences(CreateValue(id1, id2)).ToArray(); + + Assert.Equal(new[] { id1, id2, schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_list_for_referenced_ids_when_null() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences(null).ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_list_from_references_field_with_schema_id_for_referenced_ids_when_other_type() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.ExtractReferences("invalid").ToArray(); + + Assert.Equal(new[] { schemaId }, result); + } + + [Fact] + public void Should_return_null_from_references_field_when_removing_references_from_null_array() + { + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); + + var result = sut.CleanReferences(null, null); + + Assert.Null(result); + } + + [Fact] + public void Should_remove_deleted_references_from_references_field() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); + + Assert.Equal(CreateValue(id1, schemaId), result); + } + + [Fact] + public void Should_remove_all_references_from_references_field_when_schema_is_removed() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId }); + + var result = sut.CleanReferences(CreateValue(id1, id2), new HashSet(new[] { schemaId })); + + Assert.Equal(CreateValue(), result); + } + + [Fact] + public void Should_return_same_token_from_references_field_when_removing_references_and_nothing_to_remove() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); + + var token = CreateValue(id1, id2); + var result = sut.CleanReferences(token, new HashSet(new[] { Guid.NewGuid() })); + + Assert.Same(token, result); + } + + private static JToken CreateValue(params Guid[] ids) + { + return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs new file mode 100644 index 000000000..9bc7060ab --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// EdmTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.GenerateEdmSchema; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.GenerateEdmSchema +{ + public class EdmTests + { + [Fact] + public void Should_escape_field_name() + { + Assert.Equal("field_name", "field-name".EscapeEdmField()); + } + + [Fact] + public void Should_unescape_field_name() + { + Assert.Equal("field-name", "field_name".UnescapeEdmField()); + } + + [Fact] + public void Should_build_edm_model() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var edmModel = TestData.MixedSchema().BuildEdmType(languagesConfig.ToResolver(), x => x); + + Assert.NotNull(edmModel); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs new file mode 100644 index 000000000..508e88d38 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// JsonSchemaTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using NJsonSchema; +using Squidex.Domain.Apps.Core.GenerateJsonSchema; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema +{ + public class JsonSchemaTests + { + private readonly Schema schema = TestData.MixedSchema(); + + [Fact] + public void Should_build_json_schema() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); + + Assert.NotNull(jsonSchema); + } + + [Fact] + public void Should_build_data_schema() + { + var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); + + var jsonSchema = schema.BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); + + Assert.NotNull(new ContentSchemaBuilder().CreateContentSchema(schema, jsonSchema)); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs similarity index 99% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 6b1f265e9..3325f39d9 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ContentDataObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Xunit; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public sealed class ContentDataObjectTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs index ec13177e4..7b957300c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintScriptEngineTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintScriptEngineTests.cs @@ -8,10 +8,11 @@ using System; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public class JintScriptEngineTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs similarity index 96% rename from tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs index af47df5f7..88d26d417 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JintUserTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/JintUserTests.cs @@ -8,12 +8,13 @@ using System.Security.Claims; using Jint; +using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure.Security; using Xunit; #pragma warning disable xUnit2004 // Do not use equality check to test for boolean conditions -namespace Squidex.Domain.Apps.Core.Scripting +namespace Squidex.Domain.Apps.Core.Operations.Scripting { public class JintUserTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs similarity index 60% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 3e293f939..6b1b43e6b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/AssetsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class AssetsFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-asset", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_assets_are_valid() { @@ -126,86 +119,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { $" contains invalid asset '{assetId}'." }); } - [Fact] - public void Should_return_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2 }, result); - } - - [Fact] - public void Should_empty_list_for_referenced_ids_when_null() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_empty_list_for_referenced_ids_when_other_type() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.GetReferencedIds("invalid").ToArray(); - - Assert.Empty(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_array() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(null, null); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_json_array() - { - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(JValue.CreateNull(), null); - - Assert.Null(result); - } - - [Fact] - public void Should_remove_deleted_references() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_return_same_token_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new AssetsField(1, "my-asset", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - var result = sut.RemoveDeletedReferences(token, new HashSet(new[] { Guid.NewGuid() })); - - Assert.Same(token, result); - } - private static JToken CreateValue(params Guid[] ids) { return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs index 8c03207be..aa96be3b7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/BooleanFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class BooleanFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-bolean", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new BooleanField(1, "name", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_null_boolean_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs similarity index 83% rename from tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 54f8f0d0f..7376cd51c 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -11,17 +11,18 @@ using System.Threading.Tasks; using FluentAssertions; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ContentValidationTests { - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); private readonly List errors = new List(); private readonly ValidationContext context = ValidationTestExtensions.ValidContext; - private Schema schema = Schema.Create("my-name", new SchemaProperties()); + private readonly Schema schema = new Schema("my-schema"); [Fact] public async Task Should_add_error_if_validating_data_with_unknown_field() @@ -43,13 +44,14 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_data_with_invalid_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { MaxValue = 100 })); var data = new NamedContentData() .AddField("my-field", new ContentFieldData() - .SetValue(1000)); + .AddValue(1000)); await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); @@ -63,7 +65,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_non_localizable_data_field_contains_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); var data = new NamedContentData() @@ -85,7 +87,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -103,7 +106,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_required_data_field_is_not_in_bag() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -120,7 +124,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -142,9 +146,12 @@ namespace Squidex.Domain.Apps.Core public async Task Should_not_add_error_if_required_field_has_no_value_for_optional_language() { var optionalConfig = - LanguagesConfig.Create(Language.ES, Language.IT).Update(Language.IT, true, false, null); + LanguagesConfig.Build( + new LanguageConfig(Language.ES, false), + new LanguageConfig(Language.IT, true)); - schema = schema.AddField(new StringField(1, "my-field", Partitioning.Language, new StringFieldProperties { IsRequired = true })); + schema.AddField(new StringField(1, "my-field", Partitioning.Language, + new StringFieldProperties { IsRequired = true })); var data = new NamedContentData() @@ -160,7 +167,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_data_contains_unsupported_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -199,13 +206,13 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_validating_partial_data_with_invalid_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { MaxValue = 100 })); var data = new NamedContentData() .AddField("my-field", new ContentFieldData() - .SetValue(1000)); + .AddValue(1000)); await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); @@ -219,7 +226,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant)); var data = new NamedContentData() @@ -241,7 +248,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -254,7 +262,8 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, new NumberFieldProperties { IsRequired = true })); + schema.AddField(new NumberField(1, "my-field", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })); var data = new NamedContentData(); @@ -267,7 +276,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_partial_data_contains_invalid_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() @@ -288,7 +297,7 @@ namespace Squidex.Domain.Apps.Core [Fact] public async Task Should_add_error_if_partial_data_contains_unsupported_language() { - schema = schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); + schema.AddField(new NumberField(1, "my-field", Partitioning.Language)); var data = new NamedContentData() diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 802cad054..5952cdfb1 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -12,9 +12,10 @@ using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; using NodaTime; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class DateTimeFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-datetime", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new DateTimeField(1, "my-datetime", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_datetime_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs similarity index 85% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index 99e305ee9..4421d93d7 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class GeolocationFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-geolocation", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_geolocation_is_valid_null() { @@ -59,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Schemas } [Fact] - public async Task Should_add_errors_if_geolocation_has_invalid_properties() + public async Task Should_add_errors_if_geolocation_has_invalid_latitude() { var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); @@ -73,6 +66,21 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { " is not a valid value." }); } + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_longitude() + { + var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 200)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value." }); + } + [Fact] public async Task Should_add_errors_if_geolocation_has_too_many_properties() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs similarity index 86% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs index e4d649ee4..4e7b963f0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs @@ -10,9 +10,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class JsonFieldTests { @@ -26,14 +27,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-json", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new JsonField(1, "my-json", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_json_is_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index f631b0f64..33978a938 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/NumberFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -11,9 +11,10 @@ using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class NumberFieldTests { @@ -27,14 +28,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-number", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new NumberField(1, "my-number", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_number_is_valid() { @@ -81,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas [Fact] public async Task Should_add_errors_if_number_is_not_allowed() { - var sut = new NumberField(1, "my-number", Partitioning.Invariant, new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) }); + var sut = new NumberField(1, "my-number", Partitioning.Invariant, new NumberFieldProperties { AllowedValues = new[] { 10d } }); await sut.ValidateAsync(CreateValue(20), errors); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs similarity index 56% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 2a0ef8df8..cbeb72435 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ReferencesFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class ReferencesFieldTests { @@ -29,14 +30,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-refs", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_references_are_valid() { @@ -127,99 +120,6 @@ namespace Squidex.Domain.Apps.Core.Schemas new[] { $" contains invalid reference '{referenceId}'." }); } - [Fact] - public void Should_return_ids() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(CreateValue(id1, id2)).ToArray(); - - Assert.Equal(new[] { id1, id2, schemaId }, result); - } - - [Fact] - public void Should_return_list_with_schema_idempty_list_for_referenced_ids_when_null() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds(null).ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_list_with_schema_id_for_referenced_ids_when_other_type() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.GetReferencedIds("invalid").ToArray(); - - Assert.Equal(new[] { schemaId }, result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_array() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(null, null); - - Assert.Null(result); - } - - [Fact] - public void Should_return_null_when_removing_references_from_null_json_array() - { - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(JValue.CreateNull(), null); - - Assert.Null(result); - } - - [Fact] - public void Should_remove_deleted_references() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { id2 })); - - Assert.Equal(CreateValue(id1), result); - } - - [Fact] - public void Should_remove_all_references_when_schema_is_removed() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = schemaId }); - - var result = sut.RemoveDeletedReferences(CreateValue(id1, id2), new HashSet(new[] { schemaId })); - - Assert.Equal(CreateValue(), result); - } - - [Fact] - public void Should_return_same_token_when_removing_references_and_nothing_to_remove() - { - var id1 = Guid.NewGuid(); - var id2 = Guid.NewGuid(); - - var sut = new ReferencesField(1, "my-refs", Partitioning.Invariant); - - var token = CreateValue(id1, id2); - var result = sut.RemoveDeletedReferences(token, new HashSet(new[] { Guid.NewGuid() })); - - Assert.Same(token, result); - } - private static JToken CreateValue(params Guid[] ids) { return ids == null ? JValue.CreateNull() : (JToken)new JArray(ids.OfType().ToArray()); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs similarity index 91% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index 951ba27fe..eda35ca87 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/StringFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -11,9 +11,10 @@ using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class StringFieldTests { @@ -27,14 +28,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-string", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new StringField(1, "my-string", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_string_is_valid() { @@ -81,7 +74,7 @@ namespace Squidex.Domain.Apps.Core.Schemas [Fact] public async Task Should_add_errors_if_string_not_allowed() { - var sut = new StringField(1, "my-string", Partitioning.Invariant, new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") }); + var sut = new StringField(1, "my-string", Partitioning.Invariant, new StringFieldProperties { AllowedValues = new[] { "Foo" } }); await sut.ValidateAsync(CreateValue("Bar"), errors); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs index 299ab5b88..946ddc64b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/TagsFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs @@ -12,9 +12,10 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Schemas; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public class TagsFieldTests { @@ -28,14 +29,6 @@ namespace Squidex.Domain.Apps.Core.Schemas Assert.Equal("my-tags", sut.Name); } - [Fact] - public void Should_clone_object() - { - var sut = new TagsField(1, "my-tags", Partitioning.Invariant); - - Assert.NotEqual(sut, sut.Enable()); - } - [Fact] public async Task Should_not_add_error_if_tags_are_valid() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs similarity index 90% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index 92876ec30..054d1c6ec 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -10,9 +10,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Schemas.Validators; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; -namespace Squidex.Domain.Apps.Core.Schemas +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public static class ValidationTestExtensions { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs similarity index 91% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs index 7c015be0f..7031e8f72 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AllowedValuesValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class AllowedValuesValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs similarity index 92% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs index f0e64e2f3..02ad49249 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionItemValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionItemValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class CollectionItemValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs index 07eac3007..f827d5be3 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/CollectionValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/CollectionValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class CollectionValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs similarity index 93% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs index 5224cdd4e..8e905315f 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/PatternValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class PatternValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs index fef036a4f..870f534fb 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RangeValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RangeValidatorTests.cs @@ -10,9 +10,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class RangeValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs similarity index 94% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs index b5a5373cd..563988671 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredStringValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public sealed class RequiredStringValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs similarity index 92% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs index 96a1a6b10..7333a0faf 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/RequiredValidatorTests.cs @@ -9,9 +9,10 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public sealed class RequiredValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs similarity index 95% rename from tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs index bed46e3be..1ce64bc0d 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs @@ -11,9 +11,10 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; -namespace Squidex.Domain.Apps.Core.Schemas.Validators +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { public class StringLengthValidatorTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs deleted file mode 100644 index c6e97c8f8..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/DateTimePropertiesTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// DateTimePropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using NodaTime; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class DateTimePropertiesTests - { - [Fact] - public void Should_provide_today_default_value() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }; - - Assert.Equal(DateTime.UtcNow.Date.ToString("o"), sut.GetDefaultValue().ToString()); - } - - [Fact] - public void Should_provide_now_default_value() - { - var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }; - - Assert.Equal(DateTime.UtcNow.ToString("o").Substring(0, 16), sut.GetDefaultValue().ToString().Substring(0, 16)); - } - - [Fact] - public void Should_provide_specific_default_value() - { - var sut = new DateTimeFieldProperties { DefaultValue = FutureDays(15) }; - - Assert.Equal(FutureDays(15).ToString(), sut.GetDefaultValue()); - } - - private static Instant FutureDays(int days) - { - return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs deleted file mode 100644 index 10b8fdd14..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldPropertiesTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -// ========================================================================== -// FieldPropertiesTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class FieldPropertiesTests - { - private readonly List errors = new List(); - - public static IEnumerable Properties - { - get - { - yield return new AssetsFieldProperties(); - yield return new BooleanFieldProperties(); - yield return new DateTimeFieldProperties(); - yield return new GeolocationFieldProperties(); - yield return new JsonFieldProperties(); - yield return new NumberFieldProperties(); - yield return new ReferencesFieldProperties(); - yield return new StringFieldProperties(); - } - } - - public static IEnumerable PropertiesData - { - get { return Properties.Select(x => new object[] { x }); } - } - - [Theory] - [MemberData(nameof(PropertiesData))] - public void Should_set_or_freeze_sut(FieldProperties properties) - { - foreach (var property in properties.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) - { - var value = - property.PropertyType.GetTypeInfo().IsValueType ? - Activator.CreateInstance(property.PropertyType) : - null; - - property.SetValue(properties, value); - - var result = property.GetValue(properties); - - Assert.Equal(value, result); - } - - properties.Freeze(); - - foreach (var property in properties.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) - { - var value = - property.PropertyType.GetTypeInfo().IsValueType ? - Activator.CreateInstance(property.PropertyType) : - null; - - Assert.Throws(() => - { - try - { - property.SetValue(properties, value); - } - catch (Exception ex) - { - throw ex.InnerException; - } - }); - } - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs deleted file mode 100644 index 07e486749..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/FieldRegistryTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// FieldRegistryTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class FieldRegistryTests - { - private readonly FieldRegistry sut = new FieldRegistry(new TypeNameRegistry()); - - private sealed class InvalidProperties : FieldProperties - { - public override JToken GetDefaultValue() - { - return null; - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return default(T); - } - } - - [Fact] - public void Should_throw_exception_if_creating_field_and_field_is_not_registered() - { - Assert.Throws(() => sut.CreateField(1, "name", Partitioning.Invariant, new InvalidProperties())); - } - - [Fact] - public void Should_create_field_by_properties() - { - var properties = new NumberFieldProperties(); - - var field = sut.CreateField(1, "name", Partitioning.Invariant, properties); - - Assert.Equal(properties, field.RawProperties); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs deleted file mode 100644 index cfac9372d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/Json/JsonSerializerTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// JsonSerializerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Immutable; -using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas.Json -{ - public class JsonSerializerTests - { - private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); - private readonly JsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - - public JsonSerializerTests() - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - - serializerSettings.ContractResolver = new ConverterContractResolver( - new InstantConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new RefTokenConverter(), - new SchemaConverter(new FieldRegistry(typeNameRegistry)), - new StringEnumConverter()); - - serializerSettings.TypeNameHandling = TypeNameHandling.Auto; - - serializer = JsonSerializer.Create(serializerSettings); - } - - [Fact] - public void Should_serialize_and_deserialize_schema() - { - var schema = - Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())).HideField(1) - .AddField(new AssetsField(2, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())).LockField(2) - .AddField(new StringField(3, "my-string1", Partitioning.Language, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ImmutableList.Create("a", "b") })) - .AddField(new StringField(4, "my-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" })) - .AddField(new NumberField(5, "my-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new BooleanField(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())).DisableField(3) - .AddField(new DateTimeField(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddField(new DateTimeField(8, "my-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) - .AddField(new ReferencesField(9, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())) - .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties())) - .Publish(); - - var deserialized = JToken.FromObject(schema, serializer).ToObject(serializer); - - deserialized.ShouldBeEquivalentTo(schema); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs deleted file mode 100644 index 6558cb88d..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/SchemaTests.cs +++ /dev/null @@ -1,281 +0,0 @@ -// ========================================================================== -// SchemaTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Newtonsoft.Json.Linq; -using NJsonSchema; -using Squidex.Domain.Apps.Core.Schemas.Edm; -using Squidex.Domain.Apps.Core.Schemas.JsonSchema; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Schemas -{ - public class SchemaTests - { - private Schema sut = Schema.Create("my-name", new SchemaProperties()); - - private sealed class InvalidProperties : FieldProperties - { - public override JToken GetDefaultValue() - { - return null; - } - - public override T Accept(IFieldPropertiesVisitor visitor) - { - return default(T); - } - } - - [Fact] - public void Should_instantiate_field() - { - var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - - var schema = Schema.Create("my-name", properties); - - Assert.Equal("my-name", schema.Name); - Assert.Equal(properties, schema.Properties); - } - - [Fact] - public void Should_throw_exception_if_creating_schema_with_invalid_name() - { - Assert.Throws(() => Schema.Create(string.Empty, new SchemaProperties())); - } - - [Fact] - public void Should_update_schema() - { - var properties = new SchemaProperties { Hints = "my-hint", Label = "my-label" }; - - sut = sut.Update(properties); - - Assert.Equal(properties, sut.Properties); - } - - [Fact] - public void Should_add_field() - { - var field = Add(); - - Assert.Equal(field, sut.FieldsById[1]); - } - - [Fact] - public void Should_throw_exception_if_adding_field_with_name_that_already_exists() - { - Add(); - - Assert.Throws(() => sut.AddField(new NumberField(2, "my-field", Partitioning.Invariant))); - } - - [Fact] - public void Should_hide_field() - { - Add(); - - sut = sut.HideField(1); - sut = sut.HideField(1); - - Assert.True(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_show_field() - { - Add(); - - sut = sut.HideField(1); - sut = sut.ShowField(1); - sut = sut.ShowField(1); - - Assert.False(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_disable_field() - { - Add(); - - sut = sut.DisableField(1); - sut = sut.DisableField(1); - - Assert.True(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - Add(); - - sut = sut.DisableField(1); - sut = sut.EnableField(1); - sut = sut.EnableField(1); - - Assert.False(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_lock_field() - { - Add(); - - sut = sut.LockField(1); - - Assert.True(sut.FieldsById[1].IsLocked); - } - - [Fact] - public void Should_delete_field() - { - Add(); - - sut = sut.DeleteField(1); - - Assert.Empty(sut.FieldsById); - } - - [Fact] - public void Should_update_field() - { - Add(); - - sut = sut.UpdateField(1, new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - Add(); - - Assert.Throws(() => sut.UpdateField(1, new StringFieldProperties())); - } - - [Fact] - public void Should_publish_schema() - { - sut = sut.Publish(); - - Assert.True(sut.IsPublished); - } - - [Fact] - public void Should_unpublish_schema() - { - sut = sut.Publish(); - sut = sut.Unpublish(); - - Assert.False(sut.IsPublished); - } - - [Fact] - public void Should_reorder_fields() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - var field3 = new StringField(3, "3", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - sut = sut.AddField(field3); - sut = sut.ReorderFields(new List { 3, 2, 1 }); - - Assert.Equal(new List { field3, field2, field1 }, sut.Fields.ToList()); - } - - [Fact] - public void Should_throw_exception_if_not_all_fields_are_covered_for_reordering() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - - Assert.Throws(() => sut.ReorderFields(new List { 1 })); - } - - [Fact] - public void Should_throw_exception_if_field_to_reorder_does_not_exist() - { - var field1 = new StringField(1, "1", Partitioning.Invariant); - var field2 = new StringField(2, "2", Partitioning.Invariant); - - sut = sut.AddField(field1); - sut = sut.AddField(field2); - - Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); - } - - [Fact] - public void Should_build_schema() - { - var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - - var jsonSchema = BuildMixedSchema().BuildJsonSchema(languagesConfig.ToResolver(), (n, s) => new JsonSchema4 { Reference = s }); - - Assert.NotNull(jsonSchema); - } - - [Fact] - public void Should_build_edm_model() - { - var languagesConfig = LanguagesConfig.Create(Language.DE, Language.EN); - - var edmModel = BuildMixedSchema().BuildEdmType(languagesConfig.ToResolver(), x => x); - - Assert.NotNull(edmModel); - } - - private static Schema BuildMixedSchema() - { - var schema = - Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new AssetsField(2, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new StringField(3, "my-string1", Partitioning.Language, - new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = ImmutableList.Create("a", "b") })) - .AddField(new StringField(4, "my-string2", Partitioning.Invariant, - new StringFieldProperties { Hints = "My String1" })) - .AddField(new NumberField(5, "my-number", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new BooleanField(6, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new DateTimeField(7, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddField(new DateTimeField(8, "my-date", Partitioning.Invariant, - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) - .AddField(new GeolocationField(9, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())) - .AddField(new ReferencesField(10, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties())) - .AddField(new TagsField(11, "my-tags", Partitioning.Invariant, - new TagsFieldProperties())); - - return schema; - } - - private NumberField Add() - { - var field = new NumberField(1, "my-field", Partitioning.Invariant); - - sut = sut.AddField(field); - - return field; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index e2b84e201..6314f5295 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -5,7 +5,8 @@ Squidex.Domain.Apps.Core - + + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs new file mode 100644 index 000000000..0930c69da --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// TestData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core +{ + public static class TestData + { + public static Schema MixedSchema() + { + var inv = Partitioning.Invariant; + + var schema = new Schema("user"); + + schema.Publish(); + schema.Update(new SchemaProperties { Hints = "The User" }); + + schema.AddField(new JsonField(1, "my-json", inv, + new JsonFieldProperties())); + + schema.AddField(new AssetsField(2, "my-assets", inv, + new AssetsFieldProperties())); + + schema.AddField(new StringField(3, "my-string1", inv, + new StringFieldProperties { Label = "My String1", IsRequired = true, AllowedValues = new[] { "a", "b" } })); + + schema.AddField(new StringField(4, "my-string2", inv, + new StringFieldProperties { Hints = "My String1" })); + + schema.AddField(new NumberField(5, "my-number", inv, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 })); + + schema.AddField(new BooleanField(6, "my-boolean", inv, + new BooleanFieldProperties())); + + schema.AddField(new DateTimeField(7, "my-datetime", inv, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })); + + schema.AddField(new DateTimeField(8, "my-date", inv, + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); + + schema.AddField(new GeolocationField(9, "my-geolocation", inv, + new GeolocationFieldProperties())); + + schema.AddField(new ReferencesField(10, "my-references", inv, + new ReferencesFieldProperties())); + + schema.AddField(new TagsField(11, "my-tags", Partitioning.Language, + new TagsFieldProperties())); + + schema.FieldsById[7].Hide(); + schema.FieldsById[8].Disable(); + schema.FieldsById[9].Lock(); + + return schema; + } + } +} From 7b9770dff18ba53f5872622ba8d66b3b5dd93e7e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 21 Oct 2017 21:19:13 +0200 Subject: [PATCH 03/10] Some progress with refactoring. --- Squidex.sln | 15 +- .../PartitioningExtensions.cs | 27 + .../Apps/Utils/AppEventDispatcher.cs | 72 +++ .../Schemas/Utils/SchemaEventDispatcher.cs | 91 +-- .../Squidex.Domain.Apps.Events.csproj | 2 +- .../Apps/MongoAppEntity.cs | 89 +-- .../Apps/MongoAppEntityClient.cs | 43 -- .../Apps/MongoAppEntityContributor.cs | 21 - .../Apps/MongoAppEntityLanguage.cs | 28 - .../Apps/MongoAppRepository.cs | 6 +- .../Apps/MongoAppRepository_EventHandling.cs | 41 +- .../Contents/Extensions.cs | 2 + .../MongoContentRepository_EventHandling.cs | 1 + .../Contents/Visitors/PropertyVisitor.cs | 2 +- .../Schemas/MongoSchemaEntity.cs | 39 +- .../Schemas/MongoSchemaRepository.cs | 12 +- .../MongoSchemaRepository_EventHandling.cs | 109 +++- .../Squidex.Domain.Apps.Read.MongoDb.csproj | 3 +- .../Apps/IAppEntity.cs | 8 +- .../Contents/Edm/EdmModelBuilder.cs | 2 +- .../Squidex.Domain.Apps.Read.csproj | 3 +- .../Apps/AppClient.cs | 56 -- .../Apps/AppClients.cs | 66 -- .../Apps/AppContributors.cs | 78 --- .../Apps/AppDomainObject.cs | 23 +- .../Contents/Commands/ContentDataCommand.cs | 12 +- .../Contents/Commands/CreateContent.cs | 7 - .../Contents/Commands/PatchContent.cs | 2 + .../Contents/Commands/UpdateContent.cs | 2 + .../Contents/ContentCommandMiddleware.cs | 6 +- .../Contents/ContentDomainObject.cs | 19 - .../Contents/Guards/GuardContent.cs | 69 ++ .../Schemas/Commands/AddField.cs | 33 +- .../Schemas/Commands/CreateSchema.cs | 55 +- .../Schemas/Commands/CreateSchemaField.cs | 33 - .../Schemas/Commands/ReorderFields.cs | 11 +- .../Schemas/Commands/UpdateField.cs | 12 +- .../Schemas/Commands/UpdateSchema.cs | 12 +- .../Guards/FieldPropertiesValidator.cs | 11 +- .../Schemas/Guards/GuardSchema.cs | 130 ++++ .../Schemas/Guards/GuardSchemaField.cs | 160 +++++ .../Schemas/Guards/SchemaFieldGuard.cs | 106 ---- .../Schemas/Guards/SchemaGuard.cs | 44 -- .../Schemas/SchemaCommandMiddleware.cs | 121 +++- .../Schemas/SchemaDomainObject.cs | 85 +-- .../Squidex.Domain.Apps.Write.csproj | 3 +- .../Webhooks/Commands/WebhookEditCommand.cs | 25 +- .../Webhooks/Guards/GuardWebhook.cs | 63 ++ .../Webhooks/WebhookCommandMiddleware.cs | 39 +- .../Webhooks/WebhookDomainObject.cs | 6 - ...{BsonConverter.cs => JsonBsonConverter.cs} | 4 +- .../MongoDb/JsonBsonSerializer.cs | 38 ++ .../MongoDb/RefTokenSerializer.cs | 17 +- src/Squidex.Infrastructure/Validate.cs | 42 ++ src/Squidex/Squidex.csproj | 1 - .../Squidex.Domain.Apps.Read.Tests.csproj | 3 +- .../Apps/AppCommandMiddlewareTests.cs | 330 ---------- .../Apps/AppDomainObjectTests.cs | 597 ------------------ .../Apps/AppEventTests.cs | 51 -- .../Assets/AssetCommandMiddlewareTests.cs | 139 ---- .../Assets/AssetDomainObjectTests.cs | 235 ------- .../Contents/ContentCommandMiddlewareTests.cs | 240 ------- .../Contents/ContentDomainObjectTests.cs | 323 ---------- .../Contents/ContentEventTests.cs | 70 -- .../Contents/ContentVersionLoaderTests.cs | 139 ---- .../NumberFieldPropertiesTests.cs | 4 +- .../StringFieldPropertiesTests.cs | 4 +- .../Schemas/Guards/GuardSchemaFieldTests.cs | 234 +++++++ .../Schemas/Guards/GuardSchemaTests.cs | 197 ++++++ .../Schemas/Guards/SchemaFieldGuardTests.cs | 169 ----- .../Schemas/Guards/SchemaGuardTests.cs | 65 -- .../Schemas/SchemaCommandMiddlewareTests.cs | 22 +- .../Schemas/SchemaDomainObjectTests.cs | 189 +----- .../Squidex.Domain.Apps.Write.Tests.csproj | 5 +- .../Webhooks/WebhookCommandMiddlewareTests.cs | 130 ---- .../Webhooks/WebhookDomainObjectTests.cs | 179 ------ 76 files changed, 1399 insertions(+), 3933 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs create mode 100644 src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityContributor.cs delete mode 100644 src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityLanguage.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/AppClient.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/AppClients.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs create mode 100644 src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs create mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs create mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaFieldGuard.cs delete mode 100644 src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaGuard.cs create mode 100644 src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs rename src/Squidex.Infrastructure.MongoDb/MongoDb/{BsonConverter.cs => JsonBsonConverter.cs} (98%) create mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs create mode 100644 src/Squidex.Infrastructure/Validate.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaFieldGuardTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaGuardTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs delete mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs diff --git a/Squidex.sln b/Squidex.sln index fd8cc9914..81f85c0f9 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -8,8 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastru EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "domain", "domain", "{4C6B06C2-6D77-4E0E-AE32-D7050236433A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core", "src\Squidex.Domain.Apps.Core\Squidex.Domain.Apps.Core.csproj", "{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure", "src\Squidex.Infrastructure\Squidex.Infrastructure.csproj", "{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Events", "src\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj", "{25F66C64-058A-4D44-BC0C-F12A054F9A91}" @@ -65,9 +63,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution stylecop.json = stylecop.json EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Model", "src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj", "{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Model", "src\Squidex.Domain.Apps.Core.Model\Squidex.Domain.Apps.Core.Model.csproj", "{F0A83301-50A5-40EA-A1A2-07C7858F5A3F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Core.Operations", "src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj", "{6B3F75B6-5888-468E-BA4F-4FC725DAEF31}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,14 +85,6 @@ Global {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|Any CPU.Build.0 = Release|Any CPU {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|x64.ActiveCfg = Release|Any CPU {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|x86.ActiveCfg = Release|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|x64.ActiveCfg = Debug|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|x86.ActiveCfg = Debug|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.Build.0 = Release|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|x64.ActiveCfg = Release|Any CPU - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|x86.ActiveCfg = Release|Any CPU {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -332,7 +322,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0} = {C9809D59-6665-471E-AD87-5AC624C65892} {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {C9809D59-6665-471E-AD87-5AC624C65892} {A85201C6-6AF8-4B63-8365-08F741050438} = {C9809D59-6665-471E-AD87-5AC624C65892} diff --git a/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs new file mode 100644 index 000000000..a7c70286c --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/PartitioningExtensions.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// PartitioningExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public static class PartitioningExtensions + { + private static readonly HashSet AllowedPartitions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + Partitioning.Language.Key, + Partitioning.Invariant.Key + }; + + public static bool IsValidPartitioning(this string value) + { + return value == null || AllowedPartitions.Contains(value); + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs new file mode 100644 index 000000000..7e4442977 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// MongoAppRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Events.Apps.Utils +{ + public static class AppEventDispatcher + { + public static void Apply(this AppContributors contributors, AppContributorRemoved @event) + { + contributors.Remove(@event.ContributorId); + } + + public static void Apply(this AppContributors contributors, AppContributorAssigned @event) + { + contributors.Assign(@event.ContributorId, @event.Permission); + } + + public static void Apply(this LanguagesConfig languagesConfig, AppLanguageAdded @event) + { + languagesConfig.Set(new LanguageConfig(@event.Language)); + } + + public static void Apply(this LanguagesConfig languagesConfig, AppLanguageRemoved @event) + { + languagesConfig.Remove(@event.Language); + } + + public static void Apply(this AppClients clients, AppClientAttached @event) + { + clients.Add(@event.Id, @event.Secret); + } + + public static void Apply(this AppClients clients, AppClientRevoked @event) + { + clients.Revoke(@event.Id); + } + + public static void Apply(this AppClients clients, AppClientRenamed @event) + { + if (clients.Clients.TryGetValue(@event.Id, out var client)) + { + client.Rename(@event.Name); + } + } + + public static void Apply(this AppClients clients, AppClientUpdated @event) + { + if (clients.Clients.TryGetValue(@event.Id, out var client)) + { + client.Update(@event.Permission); + } + } + + public static void Apply(this LanguagesConfig languagesConfig, AppLanguageUpdated @event) + { + languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, @event.Fallback)); + + if (@event.IsMaster) + { + languagesConfig.MakeMaster(@event.Language); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs index 924048c21..0ff340140 100644 --- a/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Schemas/Utils/SchemaEventDispatcher.cs @@ -14,9 +14,14 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils { public static class SchemaEventDispatcher { - public static Schema Dispatch(SchemaCreated @event, FieldRegistry registry) + public static Schema Create(SchemaCreated @event, FieldRegistry registry) { - var schema = Schema.Create(@event.Name, @event.Properties); + var schema = new Schema(@event.Name); + + if (@event.Properties != null) + { + schema.Update(@event.Properties); + } if (@event.Fields != null) { @@ -33,20 +38,20 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils if (eventField.IsHidden) { - field = field.Hide(); + field.Hide(); } if (eventField.IsDisabled) { - field = field.Disable(); + field.Disable(); } if (eventField.IsLocked) { - field = field.Lock(); + field.Lock(); } - schema = schema.AddField(field); + schema.AddField(field); fieldId++; } @@ -55,7 +60,7 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils return schema; } - public static Schema Dispatch(FieldAdded @event, Schema schema, FieldRegistry registry) + public static void Apply(this Schema schema, FieldAdded @event, FieldRegistry registry) { var partitioning = string.Equals(@event.Partitioning, Partitioning.Language.Key, StringComparison.OrdinalIgnoreCase) ? @@ -65,69 +70,81 @@ namespace Squidex.Domain.Apps.Events.Schemas.Utils var fieldId = @event.FieldId.Id; var field = registry.CreateField(fieldId, @event.Name, partitioning, @event.Properties); - if (schema.FieldsById.ContainsKey(fieldId)) - { - return schema.UpdateField(fieldId, f => field); - } - else - { - return schema.AddField(field); - } + schema.DeleteField(fieldId); + schema.AddField(field); } - public static Schema Dispatch(FieldUpdated @event, Schema schema) + public static void Apply(this Schema schema, FieldUpdated @event) { - return schema.UpdateField(@event.FieldId.Id, @event.Properties); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Update(@event.Properties); + } } - public static Schema Dispatch(FieldLocked @event, Schema schema) + public static void Apply(this Schema schema, FieldLocked @event) { - return schema.LockField(@event.FieldId.Id); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Lock(); + } } - public static Schema Dispatch(FieldHidden @event, Schema schema) + public static void Apply(this Schema schema, FieldHidden @event) { - return schema.HideField(@event.FieldId.Id); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Hide(); + } } - public static Schema Dispatch(FieldShown @event, Schema schema) + public static void Apply(this Schema schema, FieldShown @event) { - return schema.ShowField(@event.FieldId.Id); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Show(); + } } - public static Schema Dispatch(FieldDisabled @event, Schema schema) + public static void Apply(this Schema schema, FieldDisabled @event) { - return schema.DisableField(@event.FieldId.Id); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Disable(); + } } - public static Schema Dispatch(FieldEnabled @event, Schema schema) + public static void Apply(this Schema schema, FieldEnabled @event) { - return schema.EnableField(@event.FieldId.Id); + if (schema.FieldsById.TryGetValue(@event.FieldId.Id, out var field)) + { + field.Enable(); + } } - public static Schema Dispatch(SchemaUpdated @event, Schema schema) + public static void Apply(this Schema schema, SchemaUpdated @event) { - return schema.Update(@event.Properties); + schema.Update(@event.Properties); } - public static Schema Dispatch(SchemaFieldsReordered @event, Schema schema) + public static void Apply(this Schema schema, SchemaFieldsReordered @event) { - return schema.ReorderFields(@event.FieldIds); + schema.ReorderFields(@event.FieldIds); } - public static Schema Dispatch(FieldDeleted @event, Schema schema) + public static void Apply(this Schema schema, FieldDeleted @event) { - return schema.DeleteField(@event.FieldId.Id); + schema.DeleteField(@event.FieldId.Id); } - public static Schema Dispatch(SchemaPublished @event, Schema schema) + public static void Apply(this Schema schema, SchemaPublished @event) { - return schema.Publish(); + schema.Publish(); } - public static Schema Dispatch(SchemaUnpublished @event, Schema schema) + public static void Apply(this Schema schema, SchemaUnpublished @event) { - return schema.Unpublish(); + schema.Unpublish(); } } } diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index af764763d..08e97944f 100644 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -7,7 +7,7 @@ True - + diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs index d2cff61c2..7810f2814 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs @@ -6,11 +6,9 @@ // All rights reserved. // ========================================================================== -using System; -using System.Collections.Generic; -using System.Linq; using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Read.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -19,10 +17,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { public sealed class MongoAppEntity : MongoEntity, IAppEntity { - private readonly IReadOnlyDictionary clientWrapper; - private readonly IReadOnlyDictionary contributorWrapper; - private LanguagesConfig languagesConfig; - [BsonRequired] [BsonElement] public string Name { get; set; } @@ -39,93 +33,28 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps [BsonElement] public string PlanOwner { get; set; } - [BsonRequired] - [BsonElement] - public string MasterLanguage { get; set; } - - [BsonRequired] + [BsonIgnoreIfDefault] [BsonElement] - public List ContributorIds { get; set; } + public string[] ContributorIds { get; set; } [BsonRequired] [BsonElement] - public List Languages { get; set; } + [BsonSerializer(typeof(JsonBsonSerializer))] + public AppClients Clients { get; set; } = new AppClients(); [BsonRequired] [BsonElement] - public Dictionary Clients { get; set; } + [BsonSerializer(typeof(JsonBsonSerializer))] + public AppContributors Contributors { get; set; } = new AppContributors(); [BsonRequired] [BsonElement] - public Dictionary Contributors { get; set; } + [BsonSerializer(typeof(JsonBsonSerializer))] + public LanguagesConfig LanguagesConfig { get; } = LanguagesConfig.Build(Language.EN); public PartitionResolver PartitionResolver { get { return LanguagesConfig.ToResolver(); } } - - public LanguagesConfig LanguagesConfig - { - get { return languagesConfig ?? (languagesConfig = CreateLanguagesConfig()); } - } - - IReadOnlyDictionary IAppEntity.Clients - { - get { return clientWrapper; } - } - - IReadOnlyDictionary IAppEntity.Contributors - { - get { return contributorWrapper; } - } - - public MongoAppEntity() - { - clientWrapper = new DictionaryWrapper(() => Clients); - - contributorWrapper = new DictionaryWrapper(() => Contributors); - } - - public void ChangePlan(string planId, RefToken planOwner) - { - PlanId = planId; - - PlanOwner = planOwner.Identifier; - } - - public void UpdateLanguages(Func updater) - { - var newConfig = updater(LanguagesConfig); - - if (languagesConfig != newConfig) - { - languagesConfig = newConfig; - Languages = newConfig.OfType().Select(FromLanguageConfig).ToList(); - - MasterLanguage = newConfig.Master.Language; - } - } - - private LanguagesConfig CreateLanguagesConfig() - { - languagesConfig = LanguagesConfig.Create(Languages?.Select(ToLanguageConfig).ToList() ?? new List()); - - if (MasterLanguage != null) - { - languagesConfig = languagesConfig.MakeMaster(MasterLanguage); - } - - return languagesConfig; - } - - private static MongoAppEntityLanguage FromLanguageConfig(LanguageConfig l) - { - return new MongoAppEntityLanguage { Iso2Code = l.Language, IsOptional = l.IsOptional, Fallback = l.LanguageFallbacks.Select(x => x.Iso2Code).ToList() }; - } - - private static LanguageConfig ToLanguageConfig(MongoAppEntityLanguage l) - { - return new LanguageConfig(l.Iso2Code, l.IsOptional, l.Fallback?.Select(f => f)); - } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs deleted file mode 100644 index fde8176cf..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityClient.cs +++ /dev/null @@ -1,43 +0,0 @@ -// ========================================================================== -// MongoAppEntityClient.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public sealed class MongoAppEntityClient : IAppClientEntity - { - [BsonRequired] - [BsonElement] - public string Id { get; set; } - - [BsonRequired] - [BsonElement] - public string Secret { get; set; } - - [BsonRequired] - [BsonElement] - public string Name { get; set; } - - [BsonRequired] - [BsonElement] - public AppClientPermission Permission { get; set; } - - string IAppClientEntity.Name - { - get { return !string.IsNullOrWhiteSpace(Name) ? Name : Id; } - } - - public MongoAppEntityClient() - { - Permission = AppClientPermission.Editor; - } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityContributor.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityContributor.cs deleted file mode 100644 index 8d4181bfe..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityContributor.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// MongoAppEntityContributor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using MongoDB.Bson.Serialization.Attributes; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Read.Apps; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public sealed class MongoAppEntityContributor : IAppContributorEntity - { - [BsonRequired] - [BsonElement] - public AppContributorPermission Permission { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityLanguage.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityLanguage.cs deleted file mode 100644 index ecdf43f98..000000000 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntityLanguage.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// MongoAppEntityLanguage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using MongoDB.Bson.Serialization.Attributes; - -namespace Squidex.Domain.Apps.Read.MongoDb.Apps -{ - public sealed class MongoAppEntityLanguage - { - [BsonRequired] - [BsonElement] - public string Iso2Code { get; set; } - - [BsonRequired] - [BsonElement] - public bool IsOptional { get; set; } - - [BsonRequired] - [BsonElement] - public List Fallback { get; set; } - } -} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs index 0b897fada..aabfa2ebd 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs @@ -8,8 +8,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson.Serialization; using MongoDB.Driver; +using Newtonsoft.Json; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Repositories; using Squidex.Infrastructure.CQRS.Events; @@ -19,9 +22,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { public partial class MongoAppRepository : MongoRepositoryBase, IAppRepository, IEventConsumer { - public MongoAppRepository(IMongoDatabase database) + public MongoAppRepository(IMongoDatabase database, JsonSerializer serializer) : base(database) { + BsonSerializer.RegisterSerializer(new JsonBsonSerializer(serializer)); } protected override string CollectionName() diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index 22166ec3f..d4d22106b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -7,13 +7,12 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Apps.Utils; using Squidex.Domain.Apps.Read.MongoDb.Utils; -using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; @@ -41,10 +40,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return Collection.CreateAsync(@event, headers, a => { - a.Clients = new Dictionary(); - a.Contributors = new Dictionary(); - a.ContributorIds = new List(); + SimpleMapper.Map(@event, a); + }); + } + protected Task On(AppPlanChanged @event, EnvelopeHeaders headers) + { + return UpdateAppAsync(@event, headers, a => + { SimpleMapper.Map(@event, a); }); } @@ -53,7 +56,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppEntityClient()); + a.Clients.Apply(@event); }); } @@ -61,7 +64,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Clients.Remove(@event.Id); + a.Clients.Apply(@event); }); } @@ -69,7 +72,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Clients[@event.Id].Name = @event.Name; + a.Clients.Apply(@event); }); } @@ -77,7 +80,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Clients[@event.Id].Permission = @event.Permission; + a.Clients.Apply(@event); }); } @@ -85,7 +88,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Contributors.Remove(@event.ContributorId); + a.Contributors.Apply(@event); }); } @@ -93,7 +96,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.Contributors[@event.ContributorId] = new MongoAppEntityContributor { Permission = @event.Permission }; + a.Contributors.Apply(@event); }); } @@ -101,7 +104,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.UpdateLanguages(c => c.Add(@event.Language)); + a.LanguagesConfig.Apply(@event); }); } @@ -109,7 +112,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.UpdateLanguages(c => c.Remove(@event.Language)); + a.LanguagesConfig.Apply(@event); }); } @@ -117,15 +120,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { return UpdateAppAsync(@event, headers, a => { - a.UpdateLanguages(c => c.Update(@event.Language, @event.IsOptional, @event.IsMaster, @event.Fallback)); - }); - } - - protected Task On(AppPlanChanged @event, EnvelopeHeaders headers) - { - return UpdateAppAsync(@event, headers, a => - { - a.ChangePlan(@event.PlanId, @event.Actor); + a.LanguagesConfig.Apply(@event); }); } @@ -135,7 +130,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { updater(a); - a.ContributorIds = a.Contributors.Keys.ToList(); + a.ContributorIds = a.Contributors.Contributors.Keys.ToArray(); }); } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs index 9a5d1ee42..bd8e91547 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs @@ -14,6 +14,8 @@ using MongoDB.Bson; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.MongoDb; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index e6e122a46..61900dd4f 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Contents; diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs index 37d93bebc..36ddad63c 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/PropertyVisitor.cs @@ -11,8 +11,8 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.OData.UriParser; using MongoDB.Driver; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Edm; namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs index bedbbbbaa..bce8e652d 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -9,8 +9,6 @@ using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Infrastructure; @@ -20,16 +18,10 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas { public sealed class MongoSchemaEntity : MongoEntity, ISchemaEntity { - private Lazy schema; - [BsonRequired] [BsonElement] public string Name { get; set; } - [BsonRequired] - [BsonElement] - public BsonDocument SchemaDocument { get; set; } - [BsonRequired] [BsonElement] public long Version { get; set; } @@ -74,32 +66,9 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas [BsonElement] public string ScriptChange { get; set; } - Schema ISchemaEntity.SchemaDef - { - get { return schema.Value; } - } - - public void SerializeSchema(Schema newSchema, JsonSerializer serializer) - { - SchemaDocument = JObject.FromObject(newSchema, serializer).ToBson(); - schema = new Lazy(() => newSchema); - - IsPublished = newSchema.IsPublished; - } - - public void UpdateSchema(JsonSerializer serializer, Func updater) - { - DeserializeSchema(serializer); - - SerializeSchema(updater(schema.Value), serializer); - } - - public void DeserializeSchema(JsonSerializer serializer) - { - if (schema == null) - { - schema = new Lazy(() => schema != null ? SchemaDocument.ToJson().ToObject(serializer) : null); - } - } + [BsonRequired] + [BsonElement] + [BsonSerializer(typeof(JsonBsonSerializer))] + public Schema SchemaDef { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs index 5e93dbcbc..a4d8ac949 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson.Serialization; using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; @@ -23,17 +24,16 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas { public partial class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, IEventConsumer { - private readonly JsonSerializer serializer; private readonly FieldRegistry registry; public MongoSchemaRepository(IMongoDatabase database, JsonSerializer serializer, FieldRegistry registry) : base(database) { Guard.NotNull(registry, nameof(registry)); - Guard.NotNull(serializer, nameof(serializer)); this.registry = registry; - this.serializer = serializer; + + BsonSerializer.RegisterSerializer(new JsonBsonSerializer(serializer)); } protected override string CollectionName() @@ -54,8 +54,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas await Collection.Find(s => s.AppId == appId && !s.IsDeleted) .ToListAsync(); - schemaEntities.ForEach(x => x.DeserializeSchema(serializer)); - return schemaEntities.OfType().ToList(); } @@ -65,8 +63,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas await Collection.Find(s => s.AppId == appId && !s.IsDeleted && s.Name == name) .FirstOrDefaultAsync(); - schemaEntity?.DeserializeSchema(serializer); - return schemaEntity; } @@ -76,8 +72,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas await Collection.Find(s => s.Id == schemaId) .FirstOrDefaultAsync(); - schemaEntity?.DeserializeSchema(serializer); - return schemaEntity; } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index 8499416d8..b4a662186 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -8,7 +8,6 @@ using System; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas.Old; @@ -41,104 +40,150 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas protected Task On(SchemaCreated @event, EnvelopeHeaders headers) { - var schema = SchemaEventDispatcher.Dispatch(@event, registry); + return Collection.CreateAsync(@event, headers, s => + { + s.SchemaDef = SchemaEventDispatcher.Create(@event, registry); - return Collection.CreateAsync(@event, headers, s => { UpdateSchema(s, schema); SimpleMapper.Map(@event, s); }); + SimpleMapper.Map(@event, s); + }); } protected Task On(FieldDeleted @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldLocked @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldHidden @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldShown @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldDisabled @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldEnabled @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldUpdated @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(SchemaFieldsReordered @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(SchemaUpdated @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(SchemaPublished @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(SchemaUnpublished @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event); + }); } protected Task On(FieldAdded @event, EnvelopeHeaders headers) { - return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry)); + return UpdateSchemaAsync(@event, headers, s => + { + s.SchemaDef.Apply(@event, registry); + }); } protected Task On(ScriptsConfigured @event, EnvelopeHeaders headers) { - return Collection.UpdateAsync(@event, headers, e => SimpleMapper.Map(@event, e)); + return Collection.UpdateAsync(@event, headers, s => + { + SimpleMapper.Map(@event, s); + }); } protected Task On(SchemaDeleted @event, EnvelopeHeaders headers) { - return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true); + return Collection.UpdateAsync(@event, headers, s => + { + s.IsDeleted = true; + }); } - private Task UpdateSchema(SquidexEvent @event, EnvelopeHeaders headers, Func updater) + protected Task On(WebhookAdded @event, EnvelopeHeaders headers) { - return Collection.UpdateAsync(@event, headers, e => UpdateSchema(e, updater)); + return Collection.UpdateAsync(@event, headers, s => + { + /* NOOP */ + }); } - private void UpdateSchema(MongoSchemaEntity entity, Func updater) + protected Task On(WebhookDeleted @event, EnvelopeHeaders headers) { - entity.UpdateSchema(serializer, updater); + return Collection.UpdateAsync(@event, headers, s => + { + /* NOOP */ + }); } - private void UpdateSchema(MongoSchemaEntity entity, Schema schema) + private Task UpdateSchemaAsync(SquidexEvent @event, EnvelopeHeaders headers, Action updater) { - entity.SerializeSchema(schema, serializer); - } + return Collection.UpdateAsync(@event, headers, s => + { + updater(s); - protected Task On(WebhookAdded @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, e => { }); - } - - protected Task On(WebhookDeleted @event, EnvelopeHeaders headers) - { - return Collection.UpdateAsync(@event, headers, e => { }); + s.IsPublished = s.SchemaDef.IsPublished; + }); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj index b32facd0e..3040f8ead 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj @@ -7,7 +7,8 @@ True - + + diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs index 8c59f8628..0eaba9695 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/IAppEntity.cs @@ -6,8 +6,8 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Read.Apps { @@ -19,11 +19,11 @@ namespace Squidex.Domain.Apps.Read.Apps string PlanOwner { get; } - LanguagesConfig LanguagesConfig { get; } + AppClients Clients { get; } - IReadOnlyDictionary Clients { get; } + AppContributors Contributors { get; } - IReadOnlyDictionary Contributors { get; } + LanguagesConfig LanguagesConfig { get; } PartitionResolver PartitionResolver { get; } } diff --git a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs index 829cb650e..14fa93c78 100644 --- a/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Read/Contents/Edm/EdmModelBuilder.cs @@ -10,8 +10,8 @@ using System; using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Edm; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Utils; diff --git a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj b/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj index ca4434061..17670622a 100644 --- a/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj +++ b/src/Squidex.Domain.Apps.Read/Squidex.Domain.Apps.Read.csproj @@ -7,7 +7,8 @@ True - + + diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs deleted file mode 100644 index b16d46982..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/AppClient.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// AppClient.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public sealed class AppClient - { - private readonly string name; - private readonly string secret; - private readonly AppClientPermission permission; - - public AppClient(string secret, string name, AppClientPermission permission) - { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNullOrEmpty(secret, nameof(secret)); - Guard.Enum(permission, nameof(permission)); - - this.name = name; - this.secret = secret; - this.permission = permission; - } - - public AppClient Update(AppClientPermission newPermission, Func message) - { - if (permission == newPermission) - { - var error = new ValidationError("Client has already the permission.", "IsReader"); - - throw new ValidationException(message(), error); - } - - return new AppClient(secret, name, newPermission); - } - - public AppClient Rename(string newName, Func message) - { - if (string.Equals(name, newName)) - { - var error = new ValidationError("Client already has the name.", "Id"); - - throw new ValidationException(message(), error); - } - - return new AppClient(secret, newName, permission); - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs deleted file mode 100644 index 0e5e36e43..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/AppClients.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// AppClients.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppClients - { - private readonly Dictionary clients = new Dictionary(); - - public void Add(string id, string secret) - { - ThrowIfFound(id, () => "Cannot add client"); - - clients[id] = new AppClient(secret, id, AppClientPermission.Editor); - } - - public void Rename(string clientId, string name) - { - ThrowIfNotFound(clientId); - - clients[clientId] = clients[clientId].Rename(name, () => "Cannot rename client"); - } - - public void Update(string clientId, AppClientPermission permission) - { - ThrowIfNotFound(clientId); - - clients[clientId] = clients[clientId].Update(permission, () => "Cannot update client"); - } - - public void Revoke(string clientId) - { - ThrowIfNotFound(clientId); - - clients.Remove(clientId); - } - - private void ThrowIfNotFound(string clientId) - { - if (!clients.ContainsKey(clientId)) - { - throw new DomainObjectNotFoundException(clientId, "Contributors", typeof(AppDomainObject)); - } - } - - private void ThrowIfFound(string clientId, Func message) - { - if (clients.ContainsKey(clientId)) - { - var error = new ValidationError("Client id is alreay part of the app.", "Id"); - - throw new ValidationException(message(), error); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs deleted file mode 100644 index 9d29f5e4c..000000000 --- a/src/Squidex.Domain.Apps.Write/Apps/AppContributors.cs +++ /dev/null @@ -1,78 +0,0 @@ -// ========================================================================== -// AppContributors.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppContributors - { - private readonly Dictionary contributors = new Dictionary(); - - public int Count - { - get { return contributors.Count; } - } - - public void Assign(string contributorId, AppContributorPermission permission) - { - string Message() => "Cannot assign contributor"; - - ThrowIfFound(contributorId, permission, Message); - ThrowIfNoOwner(c => c[contributorId] = permission, Message); - - contributors[contributorId] = permission; - } - - public void Remove(string contributorId) - { - string Message() => "Cannot remove contributor"; - - ThrowIfNotFound(contributorId); - ThrowIfNoOwner(c => c.Remove(contributorId), Message); - - contributors.Remove(contributorId); - } - - private void ThrowIfNotFound(string contributorId) - { - if (!contributors.ContainsKey(contributorId)) - { - throw new DomainObjectNotFoundException(contributorId, "Contributors", typeof(AppDomainObject)); - } - } - - private void ThrowIfFound(string contributorId, AppContributorPermission permission, Func message) - { - if (contributors.TryGetValue(contributorId, out var currentPermission) && currentPermission == permission) - { - var error = new ValidationError("Contributor is already part of the app with same permissions.", "ContributorId"); - - throw new ValidationException(message(), error); - } - } - - private void ThrowIfNoOwner(Action> change, Func message) - { - var contributorsCopy = new Dictionary(contributors); - - change(contributorsCopy); - - if (contributorsCopy.All(x => x.Value != AppContributorPermission.Owner)) - { - var error = new ValidationError("Contributor is the last owner.", "ContributorId"); - - throw new ValidationException(message(), error); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs index 4c217c29c..46a5b486c 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs @@ -11,6 +11,7 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Apps.Utils; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS; @@ -25,7 +26,7 @@ namespace Squidex.Domain.Apps.Write.Apps private static readonly Language DefaultLanguage = Language.EN; private readonly AppContributors contributors = new AppContributors(); private readonly AppClients clients = new AppClients(); - private LanguagesConfig languagesConfig = LanguagesConfig.Empty; + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(DefaultLanguage); private string name; private string planId; private RefToken planOwner; @@ -42,7 +43,7 @@ namespace Squidex.Domain.Apps.Write.Apps public int ContributorCount { - get { return contributors.Count; } + get { return contributors.Contributors.Count; } } public AppDomainObject(Guid id, int version) @@ -57,47 +58,47 @@ namespace Squidex.Domain.Apps.Write.Apps protected void On(AppContributorAssigned @event) { - contributors.Assign(@event.ContributorId, @event.Permission); + contributors.Apply(@event); } protected void On(AppContributorRemoved @event) { - contributors.Remove(@event.ContributorId); + contributors.Apply(@event); } protected void On(AppClientAttached @event) { - clients.Add(@event.Id, @event.Secret); + clients.Apply(@event); } protected void On(AppClientUpdated @event) { - clients.Update(@event.Id, @event.Permission); + clients.Apply(@event); } protected void On(AppClientRenamed @event) { - clients.Rename(@event.Id, @event.Name); + clients.Apply(@event); } protected void On(AppClientRevoked @event) { - clients.Revoke(@event.Id); + clients.Apply(@event); } protected void On(AppLanguageAdded @event) { - languagesConfig = languagesConfig.Add(@event.Language); + languagesConfig.Apply(@event); } protected void On(AppLanguageRemoved @event) { - languagesConfig = languagesConfig.Remove(@event.Language); + languagesConfig.Apply(@event); } protected void On(AppLanguageUpdated @event) { - languagesConfig = languagesConfig.Update(@event.Language, @event.IsOptional, @event.IsMaster, @event.Fallback); + languagesConfig.Apply(@event); } protected void On(AppPlanChanged @event) diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs index d80886ed9..02c147574 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentDataCommand.cs @@ -6,22 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Contents; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Contents.Commands { - public abstract class ContentDataCommand : ContentCommand, IValidatable + public abstract class ContentDataCommand : ContentCommand { public NamedContentData 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.Domain.Apps.Write/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs index bbab13583..3065c0202 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/CreateContent.cs @@ -6,17 +6,10 @@ // All rights reserved. // ========================================================================== -using System; - namespace Squidex.Domain.Apps.Write.Contents.Commands { public sealed class CreateContent : ContentDataCommand { public bool Publish { get; set; } - - public CreateContent() - { - ContentId = Guid.NewGuid(); - } } } diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs index ff38a5638..410315879 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs @@ -6,6 +6,8 @@ // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; + namespace Squidex.Domain.Apps.Write.Contents.Commands { public sealed class PatchContent : ContentDataCommand diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs index be9546173..47d6128ad 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs @@ -6,6 +6,8 @@ // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Contents; + namespace Squidex.Domain.Apps.Write.Contents.Commands { public sealed class UpdateContent : ContentDataCommand diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 934758bfd..810eb7b8e 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -10,9 +10,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Assets.Repositories; @@ -147,8 +147,6 @@ namespace Squidex.Domain.Apps.Write.Contents private async Task ValidateAsync((ISchemaEntity Schema, IAppEntity App) schemaAndApp, ContentDataCommand command, Func message, bool partial) { - Guard.Valid(command, nameof(command), message); - var schemaErrors = new List(); var appId = command.AppId.Id; diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs index a9fc44fb4..0857bd78e 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentDomainObject.cs @@ -69,8 +69,6 @@ namespace Squidex.Domain.Apps.Write.Contents public ContentDomainObject Create(CreateContent command) { - Guard.Valid(command, nameof(command), () => "Cannot create content"); - VerifyNotCreated(); RaiseEvent(SimpleMapper.Map(command, new ContentCreated())); @@ -85,8 +83,6 @@ namespace Squidex.Domain.Apps.Write.Contents public ContentDomainObject Delete(DeleteContent command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new ContentDeleted())); @@ -96,10 +92,7 @@ namespace Squidex.Domain.Apps.Write.Contents public ContentDomainObject ChangeStatus(ChangeContentStatus command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - VerifyCanChangeStatus(command.Status); RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged())); @@ -108,8 +101,6 @@ namespace Squidex.Domain.Apps.Write.Contents public ContentDomainObject Update(UpdateContent command) { - Guard.Valid(command, nameof(command), () => "Cannot update content"); - VerifyCreatedAndNotDeleted(); if (!command.Data.Equals(Data)) @@ -122,8 +113,6 @@ namespace Squidex.Domain.Apps.Write.Contents public ContentDomainObject Patch(PatchContent command) { - Guard.Valid(command, nameof(command), () => "Cannot patch content"); - VerifyCreatedAndNotDeleted(); var newData = Data.MergeInto(command.Data); @@ -136,14 +125,6 @@ namespace Squidex.Domain.Apps.Write.Contents return this; } - private void VerifyCanChangeStatus(Status newStatus) - { - if (!StatusFlow.Exists(newStatus) || !StatusFlow.CanChange(status, newStatus)) - { - throw new DomainException($"Content cannot be changed from status {status} to {newStatus}."); - } - } - private void VerifyNotCreated() { if (isCreated) diff --git a/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs new file mode 100644 index 000000000..28b7736ce --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs @@ -0,0 +1,69 @@ +// ========================================================================== +// GuardContent.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Contents.Guards +{ + public static class GuardContent + { + public static void CanCreate(CreateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot created content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanCreate(UpdateContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot update content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanCreate(PatchContent command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot patch content.", error => + { + if (command.Data == null) + { + error(new ValidationError("Data cannot be null.", nameof(command.Data))); + } + }); + } + + public static void CanChangeStatus(Status status, ChangeContentStatus command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot change status.", error => + { + if (!StatusFlow.Exists(command.Status) || !StatusFlow.CanChange(status, command.Status)) + { + error(new ValidationError($"Content cannot be changed from status {status} to {command.Status}.", nameof(command.Status))); + } + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs index d0e7b248a..8b78836df 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/AddField.cs @@ -6,47 +6,16 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Guards; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Schemas.Commands { - public sealed class AddField : SchemaAggregateCommand, IValidatable + public sealed class AddField : SchemaAggregateCommand { public string Name { get; set; } public string Partitioning { get; set; } public FieldProperties Properties { get; set; } - - public void Validate(IList errors) - { - if (!Partitioning.IsValidPartitioning()) - { - errors.Add(new ValidationError("Partitioning is not valid.", nameof(Partitioning))); - } - - if (!Name.IsPropertyName()) - { - errors.Add(new ValidationError("Name must be a valid property name.", nameof(Name))); - } - - if (Properties == null) - { - errors.Add(new ValidationError("Properties must be defined.", nameof(Properties))); - } - else - { - var propertyErrors = FieldPropertiesValidator.Validate(Properties); - - foreach (var error in propertyErrors) - { - errors.Add(error); - } - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs index d2fa51a4f..a037119df 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchema.cs @@ -7,45 +7,19 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using SchemaFields = System.Collections.Generic.List; namespace Squidex.Domain.Apps.Write.Schemas.Commands { - public sealed class CreateSchema : AppCommand, IValidatable, IAggregateCommand + public sealed class CreateSchema : AppCommand, IAggregateCommand { - private SchemaProperties properties; - private SchemaFields fields; - public Guid SchemaId { get; set; } - public SchemaProperties Properties - { - get - { - return properties ?? (properties = new SchemaProperties()); - } - set - { - properties = value; - } - } + public SchemaFields Fields { get; set; } - public SchemaFields Fields - { - get - { - return fields ?? (fields = new SchemaFields()); - } - set - { - fields = value; - } - } + public SchemaProperties Properties { get; set; } public string Name { get; set; } @@ -58,28 +32,5 @@ namespace Squidex.Domain.Apps.Write.Schemas.Commands { SchemaId = Guid.NewGuid(); } - - public void Validate(IList errors) - { - if (!Name.IsSlug()) - { - errors.Add(new ValidationError("Name must be a valid slug.", nameof(Name))); - } - - if (Fields.Any()) - { - var index = 0; - - foreach (var field in Fields) - { - field.Validate(index++, errors); - } - - if (Fields.Select(x => x.Name).Distinct().Count() != Fields.Count) - { - errors.Add(new ValidationError("Fields cannot have duplicate names.", nameof(Fields))); - } - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs index 45a9f3c2b..d532a0a76 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/CreateSchemaField.cs @@ -6,11 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Write.Schemas.Guards; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Schemas.Commands { @@ -27,34 +23,5 @@ namespace Squidex.Domain.Apps.Write.Schemas.Commands public bool IsDisabled { get; set; } public FieldProperties Properties { get; set; } - - public void Validate(int index, IList errors) - { - var prefix = $"Fields.{index}"; - - if (!Partitioning.IsValidPartitioning()) - { - errors.Add(new ValidationError("Partitioning is not valid.", $"{prefix}.{nameof(Partitioning)}")); - } - - if (!Name.IsPropertyName()) - { - errors.Add(new ValidationError("Name must be a valid property name.", $"{prefix}.{nameof(Name)}")); - } - - if (Properties == null) - { - errors.Add(new ValidationError("Properties must be defined.", $"{prefix}.{nameof(Properties)}")); - } - else - { - var propertyErrors = FieldPropertiesValidator.Validate(Properties); - - foreach (var error in propertyErrors) - { - errors.Add(error); - } - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs index c930b6f09..2d3805fb1 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/ReorderFields.cs @@ -7,20 +7,11 @@ // ========================================================================== using System.Collections.Generic; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Schemas.Commands { - public sealed class ReorderFields : SchemaAggregateCommand, IValidatable + public sealed class ReorderFields : SchemaAggregateCommand { public List FieldIds { get; set; } - - public void Validate(IList errors) - { - if (FieldIds == null) - { - errors.Add(new ValidationError("Field ids must be specified.", nameof(FieldIds))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs index 1f302c132..1f70668cb 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateField.cs @@ -6,22 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Schemas.Commands { - public sealed class UpdateField : FieldCommand, IValidatable + public sealed class UpdateField : FieldCommand { public FieldProperties Properties { get; set; } - - public void Validate(IList errors) - { - if (Properties == null) - { - errors.Add(new ValidationError("Properties must be defined.", nameof(Properties))); - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs index 4cfa1ebc3..ba711a5a0 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Commands/UpdateSchema.cs @@ -6,22 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Schemas.Commands { - public sealed class UpdateSchema : SchemaAggregateCommand, IValidatable + public sealed class UpdateSchema : SchemaAggregateCommand { public SchemaProperties Properties { get; set; } - - public void Validate(IList errors) - { - if (Properties == null) - { - errors.Add(new ValidationError("Properties must be specified.", nameof(Properties))); - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs index c73c34f13..04b10fe84 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/FieldPropertiesValidator.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; @@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards public static IEnumerable Validate(FieldProperties properties) { - return properties.Accept(Instance); + return properties?.Accept(Instance) ?? Enumerable.Empty(); } public IEnumerable Visit(AssetsFieldProperties properties) @@ -110,7 +111,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Length == 0)) { yield return new ValidationError("Radio buttons or dropdown list need allowed values.", nameof(properties.AllowedValues)); @@ -135,7 +136,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.MaxValue)); } - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) + if (properties.AllowedValues != null && properties.AllowedValues.Length > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) { yield return new ValidationError("Either allowed values or min and max value can be defined.", nameof(properties.AllowedValues), @@ -162,7 +163,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Length == 0)) { yield return new ValidationError("Radio buttons or dropdown list need allowed values.", nameof(properties.AllowedValues)); @@ -181,7 +182,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards nameof(properties.MaxLength)); } - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) + if (properties.AllowedValues != null && properties.AllowedValues.Length > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) { yield return new ValidationError("Either allowed values or min and max length can be defined.", nameof(properties.AllowedValues), diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs new file mode 100644 index 000000000..ed0e69533 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// GuardSchema.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Schemas.Guards +{ + public static class GuardSchema + { + public static Task CanCreate(CreateSchema command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create schema.", async error => + { + if (!command.Name.IsSlug()) + { + error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); + } + + if (await schemas.FindSchemaByNameAsync(command.AppId.Id, command.Name) != null) + { + error(new ValidationError($"A schema with name '{command.Name}' already exists", nameof(command.Name))); + } + + if (command.Fields != null && command.Fields.Any()) + { + var index = 0; + + foreach (var field in command.Fields) + { + var prefix = $"Fields.{index}"; + + if (!field.Partitioning.IsValidPartitioning()) + { + error(new ValidationError("Partitioning is not valid.", $"{prefix}.{nameof(field.Partitioning)}")); + } + + if (!field.Name.IsPropertyName()) + { + error(new ValidationError("Name must be a valid property name.", $"{prefix}.{nameof(field.Name)}")); + } + + if (field.Properties == null) + { + error(new ValidationError("Properties must be defined.", $"{prefix}.{nameof(field.Properties)}")); + } + + var propertyErrors = FieldPropertiesValidator.Validate(field.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + } + + if (command.Fields.Select(x => x.Name).Distinct().Count() != command.Fields.Count) + { + error(new ValidationError("Fields cannot have duplicate names.", nameof(command.Fields))); + } + } + }); + } + + public static void CanReorder(Schema schema, ReorderFields command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot reorder schema fields.", error => + { + if (command.FieldIds == null) + { + error(new ValidationError("Field ids must be specified.", nameof(command.FieldIds))); + } + + if (command.FieldIds.Count != schema.Fields.Count || command.FieldIds.Any(x => !schema.FieldsById.ContainsKey(x))) + { + error(new ValidationError("Ids must cover all fields.", nameof(command.FieldIds))); + } + }); + } + + public static void CanPublish(Schema schema, PublishSchema command) + { + Guard.NotNull(command, nameof(command)); + + if (schema.IsPublished) + { + throw new DomainException("Schema is already published."); + } + } + + public static void CanUnpublish(Schema schema, UnpublishSchema command) + { + Guard.NotNull(command, nameof(command)); + + if (!schema.IsPublished) + { + throw new DomainException("Schema is not published."); + } + } + + public static void CanUpdate(Schema schema, UpdateSchema command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanConfigureScripts(Schema schema, ConfigureScripts command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanDelete(Schema schema, DeleteSchema command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs new file mode 100644 index 000000000..b02f32d45 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs @@ -0,0 +1,160 @@ +// ========================================================================== +// SchemaFieldGuard.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Schemas.Guards +{ + public static class GuardSchemaField + { + public static void CanAdd(Schema schema, AddField command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add a new field.", error => + { + if (!command.Partitioning.IsValidPartitioning()) + { + error(new ValidationError("Partitioning is not valid.", nameof(command.Partitioning))); + } + + if (!command.Name.IsPropertyName()) + { + error(new ValidationError("Name must be a valid property name.", nameof(command.Name))); + } + + if (command.Properties == null) + { + error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + } + + var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + + if (schema.FieldsByName.ContainsKey(command.Name)) + { + error(new ValidationError($"There is already a field with name '{command.Name}'", nameof(command.Name))); + } + }); + } + + public static void CanUpdate(Schema schema, UpdateField command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot update field.", error => + { + if (command.Properties == null) + { + error(new ValidationError("Properties must be defined.", nameof(command.Properties))); + } + + var propertyErrors = FieldPropertiesValidator.Validate(command.Properties); + + foreach (var propertyError in propertyErrors) + { + error(propertyError); + } + }); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + + public static void CanDelete(Schema schema, DeleteField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is locked."); + } + } + + public static void CanHide(Schema schema, HideField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsHidden) + { + throw new DomainException("Schema field is already hidden."); + } + } + + public static void CanShow(Schema schema, ShowField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (!field.IsHidden) + { + throw new DomainException("Schema field is already visible."); + } + } + + public static void CanDisable(Schema schema, DisableField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsDisabled) + { + throw new DomainException("Schema field is already disabled."); + } + } + + public static void CanEnable(Schema schema, EnableField command) + { + var field = GetFieldOrThrow(schema, command.FieldId); + + if (!field.IsDisabled) + { + throw new DomainException("Schema field is already enabled."); + } + } + + public static void CanLock(Schema schema, LockField command) + { + Guard.NotNull(command, nameof(command)); + + var field = GetFieldOrThrow(schema, command.FieldId); + + if (field.IsLocked) + { + throw new DomainException("Schema field is already locked."); + } + } + + private static Field GetFieldOrThrow(Schema schema, long fieldId) + { + if (!schema.FieldsById.TryGetValue(fieldId, out var field)) + { + throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Field)); + } + + return field; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaFieldGuard.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaFieldGuard.cs deleted file mode 100644 index 9e6f320d3..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaFieldGuard.cs +++ /dev/null @@ -1,106 +0,0 @@ -// ========================================================================== -// SchemaFieldGuard.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public static class SchemaFieldGuard - { - public static void GuardCanAdd(Schema schema, string name) - { - if (schema.FieldsByName.ContainsKey(name)) - { - var error = new ValidationError($"There is already a field with name '{name}'", "Name"); - - throw new ValidationException("Cannot add a new field.", error); - } - } - - public static void GuardCanDelete(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is locked."); - } - } - - public static void GuardCanHide(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (field.IsHidden) - { - throw new DomainException("Schema field is already hidden."); - } - } - - public static void GuardCanShow(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (!field.IsHidden) - { - throw new DomainException("Schema field is already visible."); - } - } - - public static void GuardCanDisable(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (field.IsDisabled) - { - throw new DomainException("Schema field is already disabled."); - } - } - - public static void GuardCanEnable(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (!field.IsDisabled) - { - throw new DomainException("Schema field is already enabled."); - } - } - - public static void GuardCanLock(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - - public static void GuardCanUpdate(Schema schema, long fieldId) - { - var field = GetFieldOrThrow(schema, fieldId); - - if (field.IsLocked) - { - throw new DomainException("Schema field is already locked."); - } - } - - private static Field GetFieldOrThrow(Schema schema, long fieldId) - { - if (!schema.FieldsById.TryGetValue(fieldId, out var field)) - { - throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Field)); - } - - return field; - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaGuard.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaGuard.cs deleted file mode 100644 index b228a7040..000000000 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/SchemaGuard.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// SchemaGuard.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public static class SchemaGuard - { - public static void GuardCanReorder(Schema schema, List fieldIds) - { - if (fieldIds.Count != schema.Fields.Count || fieldIds.Any(x => !schema.FieldsById.ContainsKey(x))) - { - var error = new ValidationError("Ids must cover all fields.", "FieldIds"); - - throw new ValidationException("Cannot reorder schema fields.", error); - } - } - - public static void GuardCanPublish(Schema schema) - { - if (schema.IsPublished) - { - throw new DomainException("Schema is already published."); - } - } - - public static void GuardCanUnpublish(Schema schema) - { - if (!schema.IsPublished) - { - throw new DomainException("Schema is not published."); - } - } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs index 62464c92e..397622cb4 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/SchemaCommandMiddleware.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Write.Schemas.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; @@ -31,19 +32,12 @@ namespace Squidex.Domain.Apps.Write.Schemas this.schemas = schemas; } - protected async Task On(CreateSchema command, CommandContext context) + protected Task On(CreateSchema command, CommandContext context) { - if (await schemas.FindSchemaByNameAsync(command.AppId.Id, command.Name) != null) + return handler.CreateAsync(context, async s => { - var error = - new ValidationError($"A schema with name '{command.Name}' already exists", "Name", - nameof(CreateSchema.Name)); + await GuardSchema.CanCreate(command, schemas); - throw new ValidationException("Cannot create a new schema.", error); - } - - await handler.CreateAsync(context, s => - { s.Create(command); context.Complete(EntityCreatedResult.Create(s.Id, s.Version)); @@ -54,75 +48,142 @@ namespace Squidex.Domain.Apps.Write.Schemas { return handler.UpdateAsync(context, s => { + GuardSchemaField.CanAdd(s.Schema, command); + s.Add(command); context.Complete(EntityCreatedResult.Create(s.Schema.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } - protected Task On(DeleteSchema command, CommandContext context) - { - return handler.UpdateAsync(context, s => s.Delete(command)); - } - protected Task On(DeleteField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.DeleteField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanDelete(s.Schema, command); + + s.DeleteField(command); + }); } protected Task On(LockField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.LockField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanLock(s.Schema, command); + + s.LockField(command); + }); } protected Task On(HideField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.HideField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanHide(s.Schema, command); + + s.HideField(command); + }); } protected Task On(ShowField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.ShowField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanShow(s.Schema, command); + + s.ShowField(command); + }); } protected Task On(DisableField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.DisableField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanDisable(s.Schema, command); + + s.DisableField(command); + }); } protected Task On(EnableField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.EnableField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanEnable(s.Schema, command); + + s.EnableField(command); + }); } - protected Task On(ReorderFields command, CommandContext context) + protected Task On(UpdateField command, CommandContext context) { - return handler.UpdateAsync(context, s => s.Reorder(command)); + return handler.UpdateAsync(context, s => + { + GuardSchemaField.CanUpdate(s.Schema, command); + + s.UpdateField(command); + }); } - protected Task On(UpdateSchema command, CommandContext context) + protected Task On(ReorderFields command, CommandContext context) { - return handler.UpdateAsync(context, s => s.Update(command)); + return handler.UpdateAsync(context, s => + { + GuardSchema.CanReorder(s.Schema, command); + + s.Reorder(command); + }); } - protected Task On(UpdateField command, CommandContext context) + protected Task On(UpdateSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => s.UpdateField(command)); + return handler.UpdateAsync(context, s => + { + GuardSchema.CanUpdate(s.Schema, command); + + s.Update(command); + }); } protected Task On(PublishSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => s.Publish(command)); + return handler.UpdateAsync(context, s => + { + GuardSchema.CanPublish(s.Schema, command); + + s.Publish(command); + }); } protected Task On(UnpublishSchema command, CommandContext context) { - return handler.UpdateAsync(context, s => s.Unpublish(command)); + return handler.UpdateAsync(context, s => + { + GuardSchema.CanUnpublish(s.Schema, command); + + s.Unpublish(command); + }); } protected Task On(ConfigureScripts command, CommandContext context) { - return handler.UpdateAsync(context, s => s.ConfigureScripts(command)); + return handler.UpdateAsync(context, s => + { + GuardSchema.CanConfigureScripts(s.Schema, command); + + s.ConfigureScripts(command); + }); + } + + protected Task On(DeleteSchema command, CommandContext context) + { + return handler.UpdateAsync(context, s => + { + GuardSchema.CanDelete(s.Schema, command); + + s.Delete(command); + }); } public async Task HandleAsync(CommandContext context, Func next) diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs index b82e139df..01cd3c162 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs @@ -12,7 +12,6 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas.Utils; using Squidex.Domain.Apps.Write.Schemas.Commands; -using Squidex.Domain.Apps.Write.Schemas.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; @@ -46,73 +45,73 @@ namespace Squidex.Domain.Apps.Write.Schemas this.registry = registry; } - public void On(FieldAdded @event) + protected void On(SchemaCreated @event) { - totalFields++; + totalFields += @event.Fields?.Count ?? 0; - schema = SchemaEventDispatcher.Dispatch(@event, schema, registry); + schema = SchemaEventDispatcher.Create(@event, registry); } - protected void On(SchemaCreated @event) + public void On(FieldAdded @event) { - totalFields += @event.Fields?.Count ?? 0; + totalFields++; - schema = SchemaEventDispatcher.Dispatch(@event, registry); + schema.Apply(@event, registry); } protected void On(FieldUpdated @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldLocked @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldHidden @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldShown @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldDisabled @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldEnabled @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(SchemaUpdated @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(FieldDeleted @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(SchemaFieldsReordered @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(SchemaPublished @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(SchemaUnpublished @event) { - schema = SchemaEventDispatcher.Dispatch(@event, schema); + schema.Apply(@event); } protected void On(SchemaDeleted @event) @@ -122,8 +121,6 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Create(CreateSchema command) { - Guard.Valid(command, nameof(command), () => "Cannot create schema"); - VerifyNotCreated(); var @event = SimpleMapper.Map(command, new SchemaCreated { SchemaId = new NamedId(Id, command.Name) }); @@ -147,12 +144,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Add(AddField command) { - Guard.Valid(command, nameof(command), () => $"Cannot add field to schema {Id}"); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanAdd(schema, command.Name); - RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(totalFields + 1, command.Name) })); return this; @@ -160,12 +153,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject UpdateField(UpdateField command) { - Guard.Valid(command, nameof(command), () => $"Cannot update schema '{Id}'"); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanUpdate(schema, command.FieldId); - RaiseEvent(command, SimpleMapper.Map(command, new FieldUpdated())); return this; @@ -173,12 +162,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject LockField(LockField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanLock(schema, command.FieldId); - RaiseEvent(command, new FieldLocked()); return this; @@ -186,12 +171,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject HideField(HideField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanHide(schema, command.FieldId); - RaiseEvent(command, new FieldHidden()); return this; @@ -199,12 +180,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject ShowField(ShowField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanShow(schema, command.FieldId); - RaiseEvent(command, new FieldShown()); return this; @@ -212,12 +189,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject DisableField(DisableField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanDisable(schema, command.FieldId); - RaiseEvent(command, new FieldDisabled()); return this; @@ -225,12 +198,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject EnableField(EnableField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanEnable(schema, command.FieldId); - RaiseEvent(command, new FieldEnabled()); return this; @@ -238,12 +207,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject DeleteField(DeleteField command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaFieldGuard.GuardCanDelete(schema, command.FieldId); - RaiseEvent(command, new FieldDeleted()); return this; @@ -251,12 +216,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Reorder(ReorderFields command) { - Guard.Valid(command, nameof(command), () => $"Cannot reorder fields for schema '{Id}'"); - VerifyCreatedAndNotDeleted(); - SchemaGuard.GuardCanReorder(schema, command.FieldIds); - RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); return this; @@ -264,12 +225,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Publish(PublishSchema command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaGuard.GuardCanPublish(schema); - RaiseEvent(SimpleMapper.Map(command, new SchemaPublished())); return this; @@ -277,12 +234,8 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Unpublish(UnpublishSchema command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); - SchemaGuard.GuardCanUnpublish(schema); - RaiseEvent(SimpleMapper.Map(command, new SchemaUnpublished())); return this; @@ -290,8 +243,6 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject ConfigureScripts(ConfigureScripts command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new ScriptsConfigured())); @@ -310,8 +261,6 @@ namespace Squidex.Domain.Apps.Write.Schemas public SchemaDomainObject Update(UpdateSchema command) { - Guard.Valid(command, nameof(command), () => $"Cannot update schema '{Id}'"); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new SchemaUpdated())); diff --git a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj b/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj index 95685bf46..0efdb97e0 100644 --- a/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj +++ b/src/Squidex.Domain.Apps.Write/Squidex.Domain.Apps.Write.csproj @@ -7,7 +7,8 @@ True - + + diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs index cb9d7503e..2f8cc5d3f 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs +++ b/src/Squidex.Domain.Apps.Write/Webhooks/Commands/WebhookEditCommand.cs @@ -9,34 +9,13 @@ using System; using System.Collections.Generic; using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Webhooks.Commands { - public abstract class WebhookEditCommand : WebhookAggregateCommand, IValidatable + public abstract class WebhookEditCommand : WebhookAggregateCommand { - private List schemas = new List(); - public Uri Url { get; set; } - public List Schemas - { - get - { - return schemas ?? (schemas = new List()); - } - set - { - schemas = value; - } - } - - public virtual void Validate(IList errors) - { - if (Url == null || !Url.IsAbsoluteUri) - { - errors.Add(new ValidationError("Url must be specified and absolute.", nameof(Url))); - } - } + public List Schemas { get; set; } = new List(); } } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs new file mode 100644 index 000000000..f8a784cb1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// GuardWebhook.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Webhooks.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Webhooks.Guards +{ + public static class GuardWebhook + { + public static Task CanCreate(CreateWebhook command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create webhook.", error => ValidateCommandAsync(command, error, schemas)); + } + + public static Task CanUpdate(UpdateWebhook command, ISchemaProvider schemas) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot update webhook.", error => ValidateCommandAsync(command, error, schemas)); + } + + public static void CanDelete(DeleteWebhook command) + { + Guard.NotNull(command, nameof(command)); + } + + private static async Task ValidateCommandAsync(WebhookEditCommand command, Action error, ISchemaProvider schemas) + { + if (command.Url == null || !command.Url.IsAbsoluteUri) + { + error(new ValidationError("Url must be specified and absolute.", nameof(command.Url))); + } + + if (command.Schemas == null) + { + error(new ValidationError("Schemas cannot be null.", nameof(command.Schemas))); + } + + var schemaErrors = await Task.WhenAll( + command.Schemas.Select(async s => + await schemas.FindSchemaByIdAsync(s.SchemaId) == null + ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) + : null)); + + foreach (var schemaError in schemaErrors.Where(x => x != null)) + { + error(schemaError); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs index 611cfeb7a..13cee7474 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookCommandMiddleware.cs @@ -7,10 +7,10 @@ // ========================================================================== using System; -using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Webhooks.Commands; +using Squidex.Domain.Apps.Write.Webhooks.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; @@ -33,21 +33,32 @@ namespace Squidex.Domain.Apps.Write.Webhooks protected async Task On(CreateWebhook command, CommandContext context) { - await ValidateAsync(command, () => "Failed to create webhook"); + await handler.CreateAsync(context, async w => + { + await GuardWebhook.CanCreate(command, schemas); - await handler.CreateAsync(context, c => c.Create(command)); + w.Create(command); + }); } protected async Task On(UpdateWebhook command, CommandContext context) { - await ValidateAsync(command, () => "Failed to update content"); + await handler.UpdateAsync(context, async c => + { + await GuardWebhook.CanUpdate(command, schemas); - await handler.UpdateAsync(context, c => c.Update(command)); + c.Update(command); + }); } protected Task On(DeleteWebhook command, CommandContext context) { - return handler.UpdateAsync(context, c => c.Delete(command)); + return handler.UpdateAsync(context, c => + { + GuardWebhook.CanDelete(command); + + c.Delete(command); + }); } public async Task HandleAsync(CommandContext context, Func next) @@ -57,21 +68,5 @@ namespace Squidex.Domain.Apps.Write.Webhooks await next(); } } - - private async Task ValidateAsync(WebhookEditCommand command, Func message) - { - var results = await Task.WhenAll( - command.Schemas.Select(async schema => - await schemas.FindSchemaByIdAsync(schema.SchemaId) == null - ? new ValidationError($"Schema {schema.SchemaId} does not exist.") - : null)); - - var errors = results.Where(x => x != null).ToArray(); - - if (errors.Length > 0) - { - throw new ValidationException(message(), errors); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs index c3484699d..fee952cf7 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Webhooks/WebhookDomainObject.cs @@ -39,8 +39,6 @@ namespace Squidex.Domain.Apps.Write.Webhooks public void Create(CreateWebhook command) { - Guard.Valid(command, nameof(command), () => "Cannot create webhook"); - VerifyNotCreated(); RaiseEvent(SimpleMapper.Map(command, new WebhookCreated())); @@ -48,8 +46,6 @@ namespace Squidex.Domain.Apps.Write.Webhooks public void Update(UpdateWebhook command) { - Guard.Valid(command, nameof(command), () => "Cannot update webhook"); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new WebhookUpdated())); @@ -57,8 +53,6 @@ namespace Squidex.Domain.Apps.Write.Webhooks public void Delete(DeleteWebhook command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new WebhookDeleted())); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonConverter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs similarity index 98% rename from src/Squidex.Infrastructure.MongoDb/MongoDb/BsonConverter.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs index 7afa68c00..100318609 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonConverter.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonConverter.cs @@ -1,5 +1,5 @@ // ========================================================================== -// BsonConverter.cs +// JsonBsonConverter.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,7 +12,7 @@ using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.MongoDb { - public static class BsonConverter + public static class JsonBsonConverter { public static BsonDocument ToBson(this JObject source) { diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs new file mode 100644 index 000000000..a2385e726 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// RefTokenSerializer.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.MongoDb +{ + public class JsonBsonSerializer : ClassSerializerBase + { + private readonly JsonSerializer serializer; + + public JsonBsonSerializer(JsonSerializer serializer) + { + Guard.NotNull(serializer, nameof(serializer)); + + this.serializer = serializer; + } + + protected override object DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + { + return BsonSerializer.Deserialize(context.Reader).ToJson().ToObject(args.NominalType, serializer); + } + + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + BsonSerializer.Serialize(context.Writer, JObject.FromObject(value, serializer).ToBson()); + } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs index 65c1ce74d..e3c62af39 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/RefTokenSerializer.cs @@ -12,7 +12,7 @@ using MongoDB.Bson.Serialization.Serializers; namespace Squidex.Infrastructure.MongoDb { - public class RefTokenSerializer : SerializerBase + public class RefTokenSerializer : ClassSerializerBase { private static readonly Lazy Registerer = new Lazy(() => { @@ -26,23 +26,16 @@ namespace Squidex.Infrastructure.MongoDb return !Registerer.IsValueCreated && Registerer.Value; } - public override RefToken Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + protected override RefToken DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) { var value = context.Reader.ReadString(); - return value != null ? RefToken.Parse(value) : null; + return RefToken.Parse(value); } - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, RefToken value) + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, RefToken value) { - if (value != null) - { - context.Writer.WriteString(value.ToString()); - } - else - { - context.Writer.WriteNull(); - } + context.Writer.WriteString(value.ToString()); } } } diff --git a/src/Squidex.Infrastructure/Validate.cs b/src/Squidex.Infrastructure/Validate.cs new file mode 100644 index 000000000..ebeaeb4a6 --- /dev/null +++ b/src/Squidex.Infrastructure/Validate.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// TypeNameRegistry.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure +{ + public static class Validate + { + public static void It(Func message, Action> action) + { + var errors = new List(); + + action(errors.Add); + + if (errors.Any()) + { + throw new ValidationException(message(), errors); + } + } + + public static async Task It(Func message, Func, Task> action) + { + var errors = new List(); + + await action(errors.Add); + + if (errors.Any()) + { + throw new ValidationException(message(), errors); + } + } + } +} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index ba48b4a0f..d7f56a3ee 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -29,7 +29,6 @@ - diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj index 24478e87f..48bba6f42 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj @@ -10,7 +10,8 @@ - + + diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs deleted file mode 100644 index 49a7b98ff..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs +++ /dev/null @@ -1,330 +0,0 @@ -// ========================================================================== -// AppCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Repositories; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Apps.Services.Implementations; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Shared.Users; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppCommandMiddlewareTests : HandlerTestBase - { - private readonly IAppRepository appRepository = A.Fake(); - private readonly IAppPlansProvider appPlansProvider = A.Fake(); - private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); - private readonly IUserResolver userResolver = A.Fake(); - private readonly AppCommandMiddleware sut; - private readonly AppDomainObject app; - private readonly Language language = Language.DE; - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientName = "client"; - - public AppCommandMiddlewareTests() - { - app = new AppDomainObject(AppId, -1); - - sut = new AppCommandMiddleware(Handler, appRepository, appPlansProvider, appPlansBillingManager, userResolver); - } - - [Fact] - public async Task Create_should_throw_exception_if_a_name_with_same_name_already_exists() - { - var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); - - A.CallTo(() => appRepository.FindAppAsync(AppName)) - .Returns(A.Dummy()); - - await TestCreate(app, async _ => - { - await Assert.ThrowsAsync(async () => await sut.HandleAsync(context)); - }, false); - - A.CallTo(() => appRepository.FindAppAsync(AppName)).MustHaveHappened(); - } - - [Fact] - public async Task Create_should_create_app_if_name_is_free() - { - var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); - - A.CallTo(() => appRepository.FindAppAsync(AppName)) - .Returns((IAppEntity)null); - - await TestCreate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(AppId, context.Result>().IdOrValue); - } - - [Fact] - public async Task AssignContributor_should_throw_exception_if_user_not_found() - { - CreateApp(); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns((IUser)null); - - await TestUpdate(app, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task AssignContributor_throw_exception_if_reached_max_contributor_size() - { - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = 2 }); - - CreateApp() - .AssignContributor(CreateCommand(new AssignContributor { ContributorId = "1" })) - .AssignContributor(CreateCommand(new AssignContributor { ContributorId = "2" })); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - A.CallTo(() => userResolver.FindByIdAsync(A.Ignored)) - .Returns(A.Dummy()); - - await TestUpdate(app, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task AssignContributor_should_throw_exception_if_null_user_not_found() - { - CreateApp(); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns((IUser)null); - - await TestUpdate(app, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task AssignContributor_should_assign_if_user_found() - { - A.CallTo(() => appPlansProvider.GetPlan(null)) - .Returns(new ConfigAppLimitsPlan { MaxContributors = -1 }); - - CreateApp(); - - var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId }); - - A.CallTo(() => userResolver.FindByIdAsync(contributorId)) - .Returns(A.Dummy()); - - 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 ChangePlan_should_throw_if_plan_not_found() - { - A.CallTo(() => appPlansProvider.IsConfiguredPlan("my-plan")) - .Returns(false); - - CreateApp() - .AttachClient(CreateCommand(new AttachClient { Id = clientName })); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [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, app.Id, app.Name, "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, app.Id, app.Name, "my-plan")) - .Returns(CreateRedirectResult()); - - CreateApp(); - - var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Null(app.PlanId); - } - - [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, app.Id, app.Name, "my-plan")).MustNotHaveHappened(); - } - - [Fact] - public async Task AddLanguage_should_update_domain_object() - { - CreateApp(); - - var context = CreateContextForCommand(new AddLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task RemoveLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new RemoveLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - [Fact] - public async Task UpdateLanguage_should_update_domain_object() - { - CreateApp() - .AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - var context = CreateContextForCommand(new UpdateLanguage { Language = language }); - - await TestUpdate(app, async _ => - { - await sut.HandleAsync(context); - }); - } - - private AppDomainObject CreateApp() - { - app.Create(CreateCommand(new CreateApp { Name = AppName })); - - return app; - } - - private static Task CreateRedirectResult() - { - return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs deleted file mode 100644 index 3301ed7e8..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs +++ /dev/null @@ -1,597 +0,0 @@ -// ========================================================================== -// AppDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Write.Apps.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppDomainObjectTests : HandlerTestBase - { - private readonly AppDomainObject sut; - private readonly string contributorId = Guid.NewGuid().ToString(); - private readonly string clientId = "client"; - private readonly string clientNewName = "My Client"; - private readonly string planId = "premium"; - - public AppDomainObjectTests() - { - sut = new AppDomainObject(AppId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - CreateApp(); - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - }); - } - - [Fact] - public void Create_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateApp())); - }); - } - - [Fact] - public void Create_should_specify_name_and_owner() - { - sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); - - Assert.Equal(AppName, sut.Name); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppCreated { Name = AppName }), - CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), - CreateEvent(new AppLanguageAdded { Language = Language.EN }) - ); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - }); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan())); - }); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_plan_configured_from_other_user() - { - CreateApp(); - - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = "other-plan", Actor = new RefToken("User", "other") })); - - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - }); - } - - [Fact] - public void ChangePlan_should_throw_exception_if_same_plan() - { - CreateApp(); - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - - Assert.Throws(() => - { - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - }); - } - - [Fact] - public void ChangePlan_should_create_events() - { - CreateApp(); - - sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppPlanChanged { PlanId = planId }) - ); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { Permission = (AppContributorPermission)123 })); - }); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_single_owner_becomes_non_owner() - { - CreateApp(); - - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = User.Identifier, Permission = AppContributorPermission.Editor })); - }); - } - - [Fact] - public void AssignContributor_should_throw_exception_if_user_already_contributor() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - - Assert.Throws(() => - { - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - }); - } - - [Fact] - public void AssignContributor_should_create_events() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) - ); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - }); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor())); - }); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_all_owners_removed() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = User.Identifier })); - }); - } - - [Fact] - public void RemoveContributor_should_throw_exception_if_contributor_not_found() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = "not-found" })); - }); - } - - [Fact] - public void RemoveContributor_should_create_events_and_remove_contributor() - { - CreateApp(); - - sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); - sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); - - sut.GetUncomittedEvents().Skip(1) - .ShouldHaveSameEvents( - CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) - ); - } - - [Fact] - public void AttachClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - }); - } - - [Fact] - public void AttachClient_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient())); - }); - - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient { Id = string.Empty })); - }); - } - - [Fact] - public void AttachClient_should_throw_exception_if_id_already_exists() - { - CreateApp(); - - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - - Assert.Throws(() => - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - }); - } - - [Fact] - public void AttachClient_should_create_events() - { - var command = new AttachClient { Id = clientId }; - - CreateApp(); - - sut.AttachClient(CreateCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) - ); - } - - [Fact] - public void RevokeClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); - }); - } - - [Fact] - public void RevokeClient_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient())); - }); - - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient { Id = string.Empty })); - }); - } - - [Fact] - public void RevokeClient_should_throw_exception_if_client_not_found() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); - }); - } - - [Fact] - public void RevokeClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRevoked { Id = clientId }) - ); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); - }); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient())); - }); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = string.Empty })); - }); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Permission = (AppClientPermission)int.MaxValue })); - }); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_not_found() - { - CreateApp(); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); - }); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_same_reader_state() - { - CreateApp(); - CreateClient(); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Permission = AppClientPermission.Editor })); - }); - } - - [Fact] - public void UpdateClient_should_throw_exception_if_same_client_name() - { - CreateApp(); - CreateClient(); - - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName })); - - Assert.Throws(() => - { - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName })); - }); - } - - [Fact] - public void UpdateClient_should_create_events() - { - CreateApp(); - CreateClient(); - - sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) - ); - } - - [Fact] - public void AddLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - }); - } - - [Fact] - public void AddLanguage_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.AddLanguage(CreateCommand(new AddLanguage())); - }); - } - - [Fact] - public void AddLanguage_should_throw_exception_if_language_already_exists() - { - CreateApp(); - - Assert.Throws(() => - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void AddLanguage_should_create_events() - { - CreateApp(); - - sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageAdded { Language = Language.DE }) - ); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage())); - }); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_language_not_found() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); - }); - } - - [Fact] - public void RemoveLanguage_should_throw_exception_if_master_language() - { - CreateApp(); - - Assert.Throws(() => - { - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void RemoveLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageRemoved { Language = Language.DE }) - ); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN })); - }); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_command_is_not_valid() - { - CreateApp(); - - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage())); - }); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_language_not_found() - { - CreateApp(); - - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE })); - }); - } - - [Fact] - public void UpdateLanguage_should_throw_exception_if_master_language() - { - CreateApp(); - - Assert.Throws(() => - { - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.EN, IsOptional = true })); - }); - } - - [Fact] - public void UpdateLanguage_should_create_events() - { - CreateApp(); - CreateLanguage(Language.DE); - - sut.UpdateLanguage(CreateCommand(new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) - ); - } - - private void CreateApp() - { - sut.Create(CreateCommand(new CreateApp { Name = AppName })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void CreateClient() - { - sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void CreateLanguage(Language language) - { - sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs deleted file mode 100644 index acb390b7e..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// AppEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Apps.Old; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Write.Apps -{ - public class AppEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - - [Fact] - public void Should_migrate_client_changed_as_reader_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = true }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); - } - - [Fact] - public void Should_migrate_client_changed_as_writer_to_client_updated() - { - var source = CreateEvent(new AppClientChanged { IsReader = false }); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); - } - - private T CreateEvent(T contentEvent) where T : AppEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs deleted file mode 100644 index 196c7aa4a..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// AssetCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.IO; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Tasks; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetCommandMiddlewareTests : HandlerTestBase - { - private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); - private readonly IAssetStore assetStore = A.Fake(); - private readonly AssetCommandMiddleware sut; - private readonly AssetDomainObject asset; - private readonly Guid assetId = Guid.NewGuid(); - private readonly Stream stream = new MemoryStream(); - private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly AssetFile file; - - public AssetCommandMiddlewareTests() - { - file = new AssetFile("my-image.png", "image/png", 1024, () => stream); - - asset = new AssetDomainObject(assetId, -1); - - sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); - } - - [Fact] - public async Task Create_should_create_asset() - { - var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); - - SetupStore(0, context.ContextId); - SetupImageInfo(); - - await TestCreate(asset, async _ => - { - await sut.HandleAsync(context); - }); - - Assert.Equal(assetId, context.Result>().IdOrValue); - - VerifyStore(0, context.ContextId); - VerifyImageInfo(); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); - - SetupStore(1, context.ContextId); - SetupImageInfo(); - - CreateAsset(); - - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(context); - }); - - VerifyStore(1, context.ContextId); - VerifyImageInfo(); - } - - [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() - { - CreateAsset(); - - var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); - - await TestUpdate(asset, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void CreateAsset() - { - asset.Create(new CreateAsset { File = file }); - } - - private void SetupImageInfo() - { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) - .Returns(image); - } - - private void SetupStore(long version, Guid commitId) - { - A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) - .Returns(TaskHelper.Done); - A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) - .Returns(TaskHelper.Done); - A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) - .Returns(TaskHelper.Done); - } - - private void VerifyImageInfo() - { - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); - } - - private void VerifyStore(long version, Guid commitId) - { - A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); - A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); - A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs deleted file mode 100644 index 58ceac5d5..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -// ========================================================================== -// AssetDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.IO; -using Squidex.Domain.Apps.Events.Assets; -using Squidex.Domain.Apps.Write.Assets.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Assets -{ - public class AssetDomainObjectTests : HandlerTestBase - { - private readonly AssetDomainObject sut; - private readonly ImageInfo image = new ImageInfo(2048, 2048); - private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); - - public Guid AssetId { get; } = Guid.NewGuid(); - - public AssetDomainObjectTests() - { - sut = new AssetDomainObject(AssetId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateAsset { File = file }); - - Assert.Throws(() => - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file })); - }); - } - - [Fact] - public void Create_should_create_events() - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image })); - - 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(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_asset_is_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset())); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateAsset(); - - sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); - - 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(() => - { - sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" })); - }); - } - - [Fact] - public void Rename_should_throw_exception_if_asset_is_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - sut.Update(CreateAssetCommand(new UpdateAsset())); - }); - } - - [Fact] - public void Rename_should_throw_exception_if_command_is_not_valid() - { - CreateAsset(); - - Assert.Throws(() => - { - sut.Rename(CreateAssetCommand(new RenameAsset())); - }); - } - - [Fact] - public void Rename_should_throw_exception_if_new_name_is_equal_to_old_name() - { - CreateAsset(); - - Assert.Throws(() => - { - sut.Rename(CreateAssetCommand(new RenameAsset { FileName = file.FileName })); - }); - } - - [Fact] - public void Rename_should_create_events() - { - CreateAsset(); - - sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateAssetCommand(new DeleteAsset())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateAsset(); - DeleteAsset(); - - Assert.Throws(() => - { - 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.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) - ); - } - - private void CreateAsset() - { - sut.Create(CreateAssetCommand(new CreateAsset { File = file })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void UpdateAsset() - { - sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteAsset() - { - sut.Delete(CreateAssetCommand(new DeleteAsset())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateAssetEvent(T @event) where T : AssetEvent - { - @event.AssetId = AssetId; - - return CreateEvent(@event); - } - - protected T CreateAssetCommand(T command) where T : AssetAggregateCommand - { - command.AssetId = AssetId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs deleted file mode 100644 index 47f591378..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ /dev/null @@ -1,240 +0,0 @@ -// ========================================================================== -// ContentCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Read.Apps; -using Squidex.Domain.Apps.Read.Apps.Services; -using Squidex.Domain.Apps.Read.Assets.Repositories; -using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentCommandMiddlewareTests : HandlerTestBase - { - private readonly ContentCommandMiddleware sut; - private readonly ContentDomainObject content; - private readonly ISchemaProvider schemas = A.Fake(); - private readonly ISchemaEntity schema = A.Fake(); - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly IAppEntity app = A.Fake(); - private readonly ClaimsPrincipal user = new ClaimsPrincipal(); - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE); - private readonly Guid contentId = Guid.NewGuid(); - - private readonly NamedContentData invalidData = - new NamedContentData() - .AddField("my-field", new ContentFieldData() - .SetValue(null)); - private readonly NamedContentData data = - new NamedContentData() - .AddField("my-field", new ContentFieldData() - .SetValue(1)); - - public ContentCommandMiddlewareTests() - { - var schemaDef = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new NumberField(1, "my-field", Partitioning.Invariant, - new NumberFieldProperties { IsRequired = true })); - - content = new ContentDomainObject(contentId, -1); - - sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), schemas, scriptEngine, A.Dummy()); - - A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); - A.CallTo(() => app.PartitionResolver).Returns(languagesConfig.ToResolver()); - - A.CallTo(() => appProvider.FindAppByIdAsync(AppId)).Returns(app); - - A.CallTo(() => schema.SchemaDef).Returns(schemaDef); - A.CallTo(() => schema.ScriptCreate).Returns(""); - A.CallTo(() => schema.ScriptChange).Returns(""); - A.CallTo(() => schema.ScriptUpdate).Returns(""); - A.CallTo(() => schema.ScriptDelete).Returns(""); - - A.CallTo(() => schemas.FindSchemaByIdAsync(SchemaId, false)).Returns(schema); - } - - [Fact] - public async Task Create_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestCreate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Create_should_create_content() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); - } - - [Fact] - public async Task Create_should_also_invoke_publish_script_when_publishing() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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>().IdOrValue); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)).Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); - } - - [Fact] - public async Task Patch_should_throw_exception_if_data_is_not_valid() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(invalidData); - - CreateContent(); - - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); - - await TestUpdate(content, async _ => - { - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - }, false); - } - - [Fact] - public async Task Patch_should_update_domain_object() - { - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) - .Returns(data); - - var patch = new NamedContentData().AddField("my-field", new ContentFieldData().SetValue(3)); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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().Data); - - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).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.Ignored, "")).MustHaveHappened(); - } - - [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.Ignored, "")).MustHaveHappened(); - } - - private void CreateContent() - { - content.Create(new CreateContent { Data = data }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs deleted file mode 100644 index 1cbeecbac..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs +++ /dev/null @@ -1,323 +0,0 @@ -// ========================================================================== -// ContentDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using FluentAssertions; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Write.Contents.Commands; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentDomainObjectTests : HandlerTestBase - { - private readonly ContentDomainObject sut; - 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)); - - public Guid ContentId { get; } = Guid.NewGuid(); - - public ContentDomainObjectTests() - { - sut = new ContentDomainObject(ContentId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateContent { Data = data }); - - Assert.Throws(() => - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - }); - } - - [Fact] - public void Create_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.Create(CreateContentCommand(new CreateContent())); - }); - } - - [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(() => - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Update(CreateContentCommand(new UpdateContent())); - }); - } - - [Fact] - public void Update_should_throw_exception_if_command_is_not_valid() - { - CreateContent(); - - Assert.Throws(() => - { - 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(() => - { - sut.Patch(CreateContentCommand(new PatchContent { Data = data })); - }); - } - - [Fact] - public void Patch_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent())); - }); - } - - [Fact] - public void Patch_should_throw_exception_if_command_is_not_valid() - { - CreateContent(); - - Assert.Throws(() => - { - sut.Patch(CreateContentCommand(new PatchContent())); - }); - } - - [Fact] - public void Patch_should_create_events() - { - CreateContent(); - - sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentUpdated { Data = otherData }) - ); - } - - [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(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_content_is_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); - }); - } - - [Fact] - public void ChangeStatus_should_throw_exception_if_status_flow_not_valid() - { - CreateContent(); - ChangeStatus(Status.Archived); - - Assert.Throws(() => - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - }); - } - - [Fact] - public void ChangeStatus_should_refresh_properties_and_create_events() - { - CreateContent(); - - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = Status.Published })); - - Assert.Equal(Status.Published, sut.Status); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateContent(); - DeleteContent(); - - Assert.Throws(() => - { - sut.Delete(CreateContentCommand(new DeleteContent())); - }); - } - - [Fact] - public void Delete_should_update_properties_create_events() - { - CreateContent(); - - sut.Delete(CreateContentCommand(new DeleteContent())); - - Assert.True(sut.IsDeleted); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateContentEvent(new ContentDeleted()) - ); - } - - private void CreateContent() - { - sut.Create(CreateContentCommand(new CreateContent { Data = data })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void UpdateContent() - { - sut.Update(CreateContentCommand(new UpdateContent { Data = data })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void ChangeStatus(Status status) - { - sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteContent() - { - sut.Delete(CreateContentCommand(new DeleteContent())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateContentEvent(T @event) where T : ContentEvent - { - @event.ContentId = ContentId; - - return CreateEvent(@event); - } - - protected T CreateContentCommand(T command) where T : ContentCommand - { - command.ContentId = ContentId; - - return CreateCommand(command); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs deleted file mode 100644 index dcdfe4fee..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// SchemaEventTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Domain.Apps.Events.Contents.Old; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -#pragma warning disable CS0612 // Type or member is obsolete - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentEventTests - { - private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); - private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); - private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); - private readonly Guid contentId = Guid.NewGuid(); - - [Fact] - public void Should_migrate_content_published_to_content_status_changed() - { - var source = CreateEvent(new ContentPublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); - } - - [Fact] - public void Should_migrate_content_unpublished_to_content_status_changed() - { - var source = CreateEvent(new ContentUnpublished()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_restored_to_content_status_changed() - { - var source = CreateEvent(new ContentRestored()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); - } - - [Fact] - public void Should_migrate_content_archived_to_content_status_changed() - { - var source = CreateEvent(new ContentArchived()); - - source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); - } - - private T CreateEvent(T contentEvent) where T : ContentEvent - { - contentEvent.Actor = actor; - contentEvent.AppId = appId; - contentEvent.SchemaId = schemaId; - contentEvent.ContentId = contentId; - - return contentEvent; - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs deleted file mode 100644 index 5a4cf45ed..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs +++ /dev/null @@ -1,139 +0,0 @@ -// ========================================================================== -// ContentVersionLoaderTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Events.Contents; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Events; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Contents -{ - public class ContentVersionLoaderTests - { - private readonly IEventStore eventStore = A.Fake(); - private readonly IStreamNameResolver nameResolver = A.Fake(); - private readonly EventDataFormatter formatter = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly Guid appId = Guid.NewGuid(); - private readonly string streamName = Guid.NewGuid().ToString(); - private readonly ContentVersionLoader sut; - - public ContentVersionLoaderTests() - { - A.CallTo(() => nameResolver.GetStreamName(typeof(ContentDomainObject), id)) - .Returns(streamName); - - sut = new ContentVersionLoader(eventStore, nameResolver, formatter); - } - - [Fact] - public async Task Should_throw_exception_when_event_store_returns_no_events() - { - A.CallTo(() => eventStore.GetEventsAsync(streamName)) - .Returns(new List()); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, -1)); - } - - [Fact] - public async Task Should_throw_exception_when_version_not_found() - { - A.CallTo(() => eventStore.GetEventsAsync(streamName)) - .Returns(new List()); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 3)); - } - - [Fact] - public async Task Should_throw_exception_when_content_is_from_another_event() - { - var eventData1 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(Guid.NewGuid(), "my-app") }; - - var events = new List - { - new StoredEvent("0", 0, eventData1) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - - await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 0)); - } - - [Fact] - public async Task Should_load_content_from_created_event() - { - var eventData1 = new EventData(); - var eventData2 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; - var event2 = new ContentStatusChanged(); - - var events = new List - { - new StoredEvent("0", 0, eventData1), - new StoredEvent("1", 1, eventData2) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - A.CallTo(() => formatter.Parse(eventData2, true)) - .Returns(new Envelope(event2)); - - var data = await sut.LoadAsync(appId, id, 3); - - Assert.Same(event1.Data, data); - } - - [Fact] - public async Task Should_load_content_from_correct_version() - { - var eventData1 = new EventData(); - var eventData2 = new EventData(); - var eventData3 = new EventData(); - - var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; - var event2 = new ContentUpdated { Data = new NamedContentData() }; - var event3 = new ContentUpdated { Data = new NamedContentData() }; - - var events = new List - { - new StoredEvent("0", 0, eventData1), - new StoredEvent("1", 1, eventData2), - new StoredEvent("2", 2, eventData3) - }; - - A.CallTo(() => eventStore.GetEventsAsync(streamName)) - .Returns(events); - - A.CallTo(() => formatter.Parse(eventData1, true)) - .Returns(new Envelope(event1)); - A.CallTo(() => formatter.Parse(eventData2, true)) - .Returns(new Envelope(event2)); - A.CallTo(() => formatter.Parse(eventData3, true)) - .Returns(new Envelope(event3)); - - var data = await sut.LoadAsync(appId, id, 1); - - Assert.Equal(event2.Data, data); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs index 727a934bd..4e299748c 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties [Fact] public void Should_add_error_if_allowed_values_and_max_value_is_specified() { - var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ImmutableList.Create(4) }; + var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = new[] { 4d } }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties [Fact] public void Should_add_error_if_allowed_values_and_min_value_is_specified() { - var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ImmutableList.Create(4) }; + var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = new[] { 4d } }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs index 19c306680..5dba533d7 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties [Fact] public void Should_add_error_if_allowed_values_and_max_value_is_specified() { - var sut = new StringFieldProperties { MinLength = 10, AllowedValues = ImmutableList.Create("4") }; + var sut = new StringFieldProperties { MinLength = 10, AllowedValues = new[] { "4" } }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties [Fact] public void Should_add_error_if_allowed_values_and_min_value_is_specified() { - var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = ImmutableList.Create("4") }; + var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = new string[] { "4" } }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs new file mode 100644 index 000000000..cf61d7f51 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaFieldTests.cs @@ -0,0 +1,234 @@ +// ========================================================================== +// GuardSchemaFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Domain.Apps.Write.Schemas.Guards; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Schemas +{ + public class GuardSchemaFieldTests + { + private readonly Schema schema = new Schema("my-schema"); + + public GuardSchemaFieldTests() + { + schema.AddField(new StringField(1, "field1", Partitioning.Invariant)); + schema.AddField(new StringField(2, "field2", Partitioning.Invariant)); + } + + [Fact] + public void CanHide_should_throw_exception_if_already_hidden() + { + var command = new HideField { FieldId = 1 }; + + schema.FieldsById[1].Hide(); + + Assert.Throws(() => GuardSchemaField.CanHide(schema, command)); + } + + [Fact] + public void CanHide_should_throw_exception_if_not_found() + { + var command = new HideField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanHide(schema, command)); + } + + [Fact] + public void CanHide_hould_not_throw_exception_if_visible() + { + var command = new HideField { FieldId = 1 }; + + GuardSchemaField.CanHide(schema, command); + } + + [Fact] + public void CanDisable_should_throw_exception_if_already_disabled() + { + var command = new DisableField { FieldId = 1 }; + + schema.FieldsById[1].Disable(); + + Assert.Throws(() => GuardSchemaField.CanDisable(schema, command)); + } + + [Fact] + public void CanDisable_should_throw_exception_if_not_found() + { + var command = new DisableField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanDisable(schema, command)); + } + + [Fact] + public void CanDisable_Should_not_throw_exception_if_enabled() + { + var command = new DisableField { FieldId = 1 }; + + GuardSchemaField.CanDisable(schema, command); + } + + [Fact] + public void CanShow_should_throw_exception_if_already_shown() + { + var command = new ShowField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema, command)); + } + + [Fact] + public void CanShow_should_throw_exception_if_not_found() + { + var command = new ShowField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanShow(schema, command)); + } + + [Fact] + public void CanShow_should_not_throw_exception_if_hidden() + { + var command = new ShowField { FieldId = 1 }; + + schema.FieldsById[1].Hide(); + + GuardSchemaField.CanShow(schema, command); + } + + [Fact] + public void CanEnable_should_throw_exception_if_already_enabled() + { + var command = new EnableField { FieldId = 1 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema, command)); + } + + [Fact] + public void CanEnable_should_throw_exception_if_not_found() + { + var command = new EnableField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanEnable(schema, command)); + } + + [Fact] + public void CanEnable_should_not_throw_exception_if_disabled() + { + var command = new EnableField { FieldId = 1 }; + + schema.FieldsById[1].Disable(); + + GuardSchemaField.CanEnable(schema, command); + } + + [Fact] + public void CanLock_should_throw_exception_if_already_locked() + { + var command = new LockField { FieldId = 1 }; + + schema.FieldsById[1].Lock(); + + Assert.Throws(() => GuardSchemaField.CanLock(schema, command)); + } + + [Fact] + public void LockField_should_throw_exception_if_not_found() + { + var command = new LockField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanLock(schema, command)); + } + + [Fact] + public void CanLock_should_not_throw_exception_if_not_locked() + { + var command = new LockField { FieldId = 1 }; + + GuardSchemaField.CanLock(schema, command); + } + + [Fact] + public void CanDelete_should_throw_exception_if_not_found() + { + var command = new DeleteField { FieldId = 3 }; + + Assert.Throws(() => GuardSchemaField.CanDelete(schema, command)); + } + + [Fact] + public void CanDelete_should_throw_exception_if_locked() + { + var command = new DeleteField { FieldId = 1 }; + + schema.FieldsById[1].Lock(); + + Assert.Throws(() => GuardSchemaField.CanDelete(schema, command)); + } + + [Fact] + public void CanDelete_should_not_throw_exception_if_not_locked() + { + var command = new DeleteField { FieldId = 1 }; + + GuardSchemaField.CanDelete(schema, command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_locked() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + + schema.FieldsById[1].Lock(); + + Assert.Throws(() => GuardSchemaField.CanUpdate(schema, command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_not_locked() + { + var command = new UpdateField { FieldId = 1, Properties = new StringFieldProperties() }; + + GuardSchemaField.CanUpdate(schema, command); + } + + [Fact] + public void CanAdd_should_throw_exception_if_field_already_exists() + { + var command = new AddField { Name = "field1", Properties = new StringFieldProperties() }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_name_not_valid() + { + var command = new AddField { Name = "INVALID_NAME", Properties = new StringFieldProperties() }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); + } + + [Fact] + public void CanAdd_should_throw_exception_if_properties_not_valid() + { + var command = new AddField { Name = "field3", Properties = new StringFieldProperties { MinLength = 10, MaxLength = 5 } }; + + Assert.Throws(() => GuardSchemaField.CanAdd(schema, command)); + } + + [Fact] + public void CanAdd_should_not_throw_exception_if_field_not_exists() + { + var command = new AddField { Name = "field3", Properties = new StringFieldProperties() }; + + GuardSchemaField.CanAdd(schema, command); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs new file mode 100644 index 000000000..901d61b67 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -0,0 +1,197 @@ +// ========================================================================== +// GuardSchemaTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Schemas.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Schemas.Guards +{ + public class GuardSchemaTests + { + private readonly ISchemaProvider schemas = A.Fake(); + private readonly Schema schema = new Schema("my-schema"); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + + public GuardSchemaTests() + { + schema.AddField(new StringField(1, "field1", Partitioning.Invariant)); + schema.AddField(new StringField(2, "field2", Partitioning.Invariant)); + + A.CallTo(() => schemas.FindSchemaByNameAsync(A.Ignored, "new-schema")) + .Returns(Task.FromResult(null)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; + + await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_name_already_in_use() + { + A.CallTo(() => schemas.FindSchemaByNameAsync(A.Ignored, "new-schema")) + .Returns(Task.FromResult(A.Fake())); + + var command = new CreateSchema { AppId = appId, Name = "new-schema" }; + + await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_fields_not_valid() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new CreateSchemaField + { + Name = null, + Properties = null, + Partitioning = "invalid", + }, + new CreateSchemaField + { + Name = null, + Properties = InvalidProperties(), + Partitioning = "invalid", + } + }, + Name = "new-schema" + }; + + await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names() + { + var command = new CreateSchema + { + AppId = appId, + Fields = new List + { + new CreateSchemaField + { + Name = "field1", + Properties = ValidProperties(), + Partitioning = "invariant" + }, + new CreateSchemaField + { + Name = "field1", + Properties = ValidProperties(), + Partitioning = "invariant" + } + }, + Name = "new-schema" + }; + + await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_command_is_valid() + { + var command = new CreateSchema { AppId = appId, Name = "new-schema" }; + + await GuardSchema.CanCreate(command, schemas); + } + + [Fact] + public void CanPublish_should_throw_exception_if_already_published() + { + var command = new PublishSchema(); + + schema.Publish(); + + Assert.Throws(() => GuardSchema.CanPublish(schema, command)); + } + + [Fact] + public void CanPublish_should_not_throw_exception_if_not_published() + { + var command = new PublishSchema(); + + GuardSchema.CanPublish(schema, command); + } + + [Fact] + public void CanUnpublish_should_throw_exception_if_already_unpublished() + { + var command = new UnpublishSchema(); + + Assert.Throws(() => GuardSchema.CanUnpublish(schema, command)); + } + + [Fact] + public void CanUnpublish_should_not_throw_exception_if_already_published() + { + var command = new UnpublishSchema(); + + schema.Publish(); + + GuardSchema.CanUnpublish(schema, command); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_contains_invalid_id() + { + var command = new ReorderFields { FieldIds = new List { 1, 3 } }; + + Assert.Throws(() => GuardSchema.CanReorder(schema, command)); + } + + [Fact] + public void CanReorder_should_throw_exception_if_field_ids_do_not_covers_all_fields() + { + var command = new ReorderFields { FieldIds = new List { 1 } }; + + Assert.Throws(() => GuardSchema.CanReorder(schema, command)); + } + + [Fact] + public void CanReorder_should_not_throw_exception_if_field_ids_are_valid() + { + var command = new ReorderFields { FieldIds = new List { 1, 2 } }; + + GuardSchema.CanReorder(schema, command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteSchema(); + + GuardSchema.CanDelete(schema, command); + } + + private static StringFieldProperties ValidProperties() + { + return new StringFieldProperties { MinLength = 10, MaxLength = 20 }; + } + + private static StringFieldProperties InvalidProperties() + { + return new StringFieldProperties { MinLength = 20, MaxLength = 10 }; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaFieldGuardTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaFieldGuardTests.cs deleted file mode 100644 index bbfcb89f9..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaFieldGuardTests.cs +++ /dev/null @@ -1,169 +0,0 @@ -// ========================================================================== -// SchemaFieldGuardTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public class SchemaFieldGuardTests - { - private Schema schema = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new StringField(1, "field1", Partitioning.Invariant)) - .AddField(new StringField(2, "field2", Partitioning.Invariant)); - - [Fact] - public void Should_throw_exception_if_field_to_hide_already_hidden() - { - schema = schema.HideField(1); - - Assert.Throws(() => SchemaFieldGuard.GuardCanHide(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_hide_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanHide(schema, 3)); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_hide_shown() - { - SchemaFieldGuard.GuardCanHide(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_disable_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanDisable(schema, 3)); - } - - [Fact] - public void Should_throw_exception_if_field_to_disable_already_disabled() - { - schema = schema.DisableField(1); - - Assert.Throws(() => SchemaFieldGuard.GuardCanDisable(schema, 1)); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_disable_shown() - { - SchemaFieldGuard.GuardCanDisable(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_show_already_shown() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanShow(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_show_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanShow(schema, 3)); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_show_hidden() - { - schema = schema.HideField(1); - - SchemaFieldGuard.GuardCanShow(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_enable_already_enabled() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanEnable(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_enable_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanEnable(schema, 3)); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_enable_disabled() - { - schema = schema.DisableField(1); - - SchemaFieldGuard.GuardCanEnable(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_lock_already_locked() - { - schema = schema.LockField(1); - - Assert.Throws(() => SchemaFieldGuard.GuardCanLock(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_lock_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanLock(schema, 3)); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_lock_not_locked() - { - SchemaFieldGuard.GuardCanLock(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_delete_not_found() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanDelete(schema, 3)); - } - - [Fact] - public void Should_throw_exception_if_field_to_delete_is_locked() - { - schema = schema.LockField(1); - - Assert.Throws(() => SchemaFieldGuard.GuardCanDelete(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_update_not_locked() - { - SchemaFieldGuard.GuardCanUpdate(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_update_is_locked() - { - schema = schema.LockField(1); - - Assert.Throws(() => SchemaFieldGuard.GuardCanUpdate(schema, 1)); - } - - [Fact] - public void Should_throw_exception_if_field_to_delete_not_locked() - { - SchemaFieldGuard.GuardCanDelete(schema, 1); - } - - [Fact] - public void Should_throw_exception_if_field_to_add_already_exists() - { - Assert.Throws(() => SchemaFieldGuard.GuardCanAdd(schema, "field1")); - } - - [Fact] - public void Should_not_throw_exception_if_field_to_add_not_exists() - { - SchemaFieldGuard.GuardCanAdd(schema, "field3"); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaGuardTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaGuardTests.cs deleted file mode 100644 index 2b60fe010..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/SchemaGuardTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// SchemaGuardTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Collections.Generic; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Schemas.Guards -{ - public class SchemaGuardTests - { - private Schema schema = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new StringField(1, "field1", Partitioning.Invariant)) - .AddField(new StringField(2, "field2", Partitioning.Invariant)); - - [Fact] - public void Should_throw_exception_if_schema_to_publish_already_published() - { - schema = schema.Publish(); - - Assert.Throws(() => SchemaGuard.GuardCanPublish(schema)); - } - - [Fact] - public void Should_not_throw_exception_if_schema_to_publish_not_published() - { - SchemaGuard.GuardCanPublish(schema); - } - - [Fact] - public void Should_throw_exception_if_schema_to_unpublish_already_unpublished() - { - Assert.Throws(() => SchemaGuard.GuardCanUnpublish(schema)); - } - - [Fact] - public void Should_not_throw_exception_if_schema_to_unpublish_published() - { - schema = schema.Publish(); - - SchemaGuard.GuardCanUnpublish(schema); - } - - [Fact] - public void Should_throw_excepotion_if_schema_fields_to_reorder_not_valid() - { - Assert.Throws(() => SchemaGuard.GuardCanReorder(schema, new List { 1 })); - Assert.Throws(() => SchemaGuard.GuardCanReorder(schema, new List { 1, 3 })); - } - - [Fact] - public void Should_not_throw_excepotion_if_schema_fields_to_reorder_are_valid() - { - SchemaGuard.GuardCanReorder(schema, new List { 1, 2 }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs index 78c4152c2..13c8b47aa 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaCommandMiddlewareTests.cs @@ -34,32 +34,16 @@ namespace Squidex.Domain.Apps.Write.Schemas schema = new SchemaDomainObject(SchemaId, -1, registry); sut = new SchemaCommandMiddleware(Handler, schemas); - } - - [Fact] - public async Task Create_should_throw_exception_if_a_name_with_same_name_already_exists() - { - var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); A.CallTo(() => schemas.FindSchemaByNameAsync(AppId, SchemaName)) - .Returns(A.Dummy()); - - await TestCreate(schema, async _ => - { - await Assert.ThrowsAsync(async () => await sut.HandleAsync(context)); - }, false); - - A.CallTo(() => schemas.FindSchemaByNameAsync(AppId, SchemaName)).MustHaveHappened(); + .Returns((ISchemaEntity)null); } [Fact] - public async Task Create_should_create_schema_if_name_is_free() + public async Task Create_should_create_schema_domain_object() { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - A.CallTo(() => schemas.FindSchemaByNameAsync(AppId, SchemaName)) - .Returns((ISchemaEntity)null); - await TestCreate(schema, async _ => { await sut.HandleAsync(context); @@ -291,4 +275,4 @@ namespace Squidex.Domain.Apps.Write.Schemas schema.DisableField(CreateCommand(new DisableField { FieldId = 1 })); } } -} +} \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs index 34a8d0f81..5fe9652a0 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/SchemaDomainObjectTests.cs @@ -44,69 +44,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void Create_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateSchema())); - }); - } - - [Fact] - public void Create_should_throw_exception_if_fields_are_not_valid() - { - var properties = new SchemaProperties(); - - var fields = new List - { - new CreateSchemaField - { - Name = null, - Properties = null, - Partitioning = "invalid", - }, - new CreateSchemaField - { - Name = null, - Properties = InvalidProperties(), - Partitioning = "invalid", - } - }; - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields })); - }); - } - - [Fact] - public void Create_should_throw_exception_if_fields_contain_duplicate_names() - { - var properties = new SchemaProperties(); - - var fields = new List - { - new CreateSchemaField - { - Name = "field1", - Properties = ValidProperties(), - Partitioning = "invariant" - }, - new CreateSchemaField - { - Name = "field1", - Properties = ValidProperties(), - Partitioning = "invariant" - } - }; - - Assert.Throws(() => - { - sut.Create(CreateCommand(new CreateSchema { Name = SchemaName, Properties = properties, Fields = fields })); - }); - } - [Fact] public void Create_should_create_schema_and_create_events() { @@ -118,7 +55,7 @@ namespace Squidex.Domain.Apps.Write.Schemas sut.GetUncomittedEvents() .ShouldHaveSameEvents( - CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties, Fields = new List() }) + CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) ); } @@ -162,17 +99,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void Update_should_throw_exception_if_command_is_not_valid() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.Update(CreateCommand(new UpdateSchema())); - }); - } - [Fact] public void Update_should_refresh_properties_and_create_events() { @@ -259,17 +185,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void Reorder_should_throw_exception_if_command_is_not_valid() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.Reorder(CreateCommand(new ReorderFields())); - }); - } - [Fact] public void Reorder_should_refresh_properties_and_create_events() { @@ -408,33 +323,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void AddField_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField())); - }); - } - - [Fact] - public void AddField_should_throw_exception_if_command_contains_invalid_partitioning() - { - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Partitioning = "invalid", Properties = ValidProperties() })); - }); - } - - [Fact] - public void AddField_should_throw_exception_if_command_contains_invalid_properties() - { - Assert.Throws(() => - { - sut.Add(CreateCommand(new AddField { Name = fieldName, Properties = InvalidProperties() })); - }); - } - [Fact] public void AddField_should_throw_exception_if_schema_is_deleted() { @@ -473,26 +361,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void UpdateField_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField())); - }); - } - - [Fact] - public void UpdateField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); - }); - } - [Fact] public void UpdateField_should_throw_exception_if_schema_is_deleted() { @@ -532,17 +400,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void LockField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.LockField(CreateCommand(new LockField { FieldId = 2 })); - }); - } - [Fact] public void LockField_should_throw_exception_if_schema_is_deleted() { @@ -580,17 +437,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void HideField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.HideField(CreateCommand(new HideField { FieldId = 2 })); - }); - } - [Fact] public void HideField_should_throw_exception_if_schema_is_deleted() { @@ -628,17 +474,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void ShowField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.ShowField(CreateCommand(new ShowField { FieldId = 2 })); - }); - } - [Fact] public void ShowField_should_throw_exception_if_schema_is_deleted() { @@ -677,17 +512,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void DisableField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.DisableField(CreateCommand(new DisableField { FieldId = 2 })); - }); - } - [Fact] public void DisableField_should_throw_exception_if_schema_is_deleted() { @@ -725,17 +549,6 @@ namespace Squidex.Domain.Apps.Write.Schemas }); } - [Fact] - public void EnableField_should_throw_exception_if_field_is_not_found() - { - CreateSchema(); - - Assert.Throws(() => - { - sut.EnableField(CreateCommand(new EnableField { FieldId = 2 })); - }); - } - [Fact] public void EnableField_should_throw_exception_if_schema_is_deleted() { diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj index 19ac1d18e..b049b5e7c 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Write.Tests/Squidex.Domain.Apps.Write.Tests.csproj @@ -5,9 +5,10 @@ Squidex.Domain.Apps.Write - - + + + diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs deleted file mode 100644 index b40b82739..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -// ========================================================================== -// WebhookCommandMiddlewareTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Domain.Apps.Core.Webhooks; -using Squidex.Domain.Apps.Read.Schemas; -using Squidex.Domain.Apps.Read.Schemas.Services; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookCommandMiddlewareTests : HandlerTestBase - { - private readonly ISchemaProvider schemas = A.Fake(); - private readonly WebhookCommandMiddleware sut; - private readonly WebhookDomainObject webhook; - private readonly Uri url = new Uri("http://squidex.io"); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid webhookId = Guid.NewGuid(); - private readonly List webhookSchemas; - - public WebhookCommandMiddlewareTests() - { - webhook = new WebhookDomainObject(webhookId, -1); - - webhookSchemas = new List - { - new WebhookSchema { SchemaId = schemaId } - }; - - sut = new WebhookCommandMiddleware(Handler, schemas); - } - - [Fact] - public async Task Create_should_create_webhook() - { - var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake()); - - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Create_should_throw_exception_when_schema_is_not_found() - { - var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); - - await Assert.ThrowsAsync(async () => - { - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - }); - } - - [Fact] - public async Task Update_should_update_domain_object() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake()); - - CreateWebhook(); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); - } - - [Fact] - public async Task Update_should_throw_exception_when_schema_is_not_found() - { - var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); - - A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); - - CreateWebhook(); - - await Assert.ThrowsAsync(async () => - { - await TestCreate(webhook, async _ => - { - await sut.HandleAsync(context); - }); - }); - } - - [Fact] - public async Task Delete_should_update_domain_object() - { - CreateWebhook(); - - var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId }); - - await TestUpdate(webhook, async _ => - { - await sut.HandleAsync(command); - }); - } - - private void CreateWebhook() - { - webhook.Create(new CreateWebhook { Url = url }); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs deleted file mode 100644 index 2e93d1ff9..000000000 --- a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs +++ /dev/null @@ -1,179 +0,0 @@ -// ========================================================================== -// WebhookDomainObjectTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Squidex.Domain.Apps.Events.Webhooks; -using Squidex.Domain.Apps.Write.TestHelpers; -using Squidex.Domain.Apps.Write.Webhooks.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; -using Xunit; - -namespace Squidex.Domain.Apps.Write.Webhooks -{ - public class WebhookDomainObjectTests : HandlerTestBase - { - private readonly Uri url = new Uri("http://squidex.io"); - private readonly WebhookDomainObject sut; - - public Guid WebhookId { get; } = Guid.NewGuid(); - - public WebhookDomainObjectTests() - { - sut = new WebhookDomainObject(WebhookId, 0); - } - - [Fact] - public void Create_should_throw_exception_if_created() - { - sut.Create(new CreateWebhook { Url = url }); - - Assert.Throws(() => - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - }); - } - - [Fact] - public void Create_should_throw_exception_if_command_is_not_valid() - { - Assert.Throws(() => - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) })); - }); - } - - [Fact] - public void Create_should_create_events() - { - var command = new CreateWebhook { Url = url }; - - sut.Create(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookCreated - { - Url = url, - Schemas = command.Schemas, - SharedSecret = command.SharedSecret, - WebhookId = command.WebhookId - }) - ); - } - - [Fact] - public void Update_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_webhook_is_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); - }); - } - - [Fact] - public void Update_should_throw_exception_if_command_is_not_valid() - { - CreateWebhook(); - - Assert.Throws(() => - { - sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) })); - }); - } - - [Fact] - public void Update_should_create_events() - { - CreateWebhook(); - - var command = new UpdateWebhook { Url = url }; - - sut.Update(CreateWebhookCommand(command)); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas }) - ); - } - - [Fact] - public void Delete_should_throw_exception_if_not_created() - { - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_throw_exception_if_already_deleted() - { - CreateWebhook(); - DeleteWebhook(); - - Assert.Throws(() => - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - }); - } - - [Fact] - public void Delete_should_update_properties_create_events() - { - CreateWebhook(); - - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - sut.GetUncomittedEvents() - .ShouldHaveSameEvents( - CreateWebhookEvent(new WebhookDeleted()) - ); - } - - private void CreateWebhook() - { - sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - private void DeleteWebhook() - { - sut.Delete(CreateWebhookCommand(new DeleteWebhook())); - - ((IAggregate)sut).ClearUncommittedEvents(); - } - - protected T CreateWebhookEvent(T @event) where T : WebhookEvent - { - @event.WebhookId = WebhookId; - - return CreateEvent(@event); - } - - protected T CreateWebhookCommand(T command) where T : WebhookAggregateCommand - { - command.WebhookId = WebhookId; - - return CreateCommand(command); - } - } -} From eb82bcc16cb3ffe5f20d09d3fd17ebf98e988b78 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 11:23:23 +0200 Subject: [PATCH 04/10] More progressive --- .../Apps/AppPlan.cs | 29 ++ .../Apps/AppCommandMiddleware.cs | 131 ++++---- .../Apps/AppDomainObject.cs | 59 +--- .../Apps/Commands/AddLanguage.cs | 11 +- .../Apps/Commands/AssignContributor.cs | 17 +- .../Apps/Commands/AttachClient.cs | 11 +- .../Apps/Commands/ChangePlan.cs | 11 +- .../Apps/Commands/CreateApp.cs | 12 +- .../Apps/Commands/RemoveContributor.cs | 13 +- .../Apps/Commands/RemoveLanguage.cs | 11 +- .../Apps/Commands/RevokeClient.cs | 13 +- .../Apps/Commands/UpdateClient.cs | 22 +- .../Apps/Commands/UpdateLanguage.cs | 10 +- .../Apps/Guards/GuardApp.cs | 65 ++++ .../Apps/Guards/GuardAppContributors.cs | 82 +++++ .../Apps/Guards/GuardAppLanguages.cs | 90 ++++++ .../Assets/AssetCommandMiddleware.cs | 19 +- .../Assets/AssetDomainObject.cs | 22 +- .../Assets/Commands/RenameAsset.cs | 13 +- .../Assets/Guards/GuardAsset.cs | 49 +++ .../Schemas/Guards/GuardSchemaField.cs | 2 +- .../Schemas/SchemaDomainObject.cs | 4 - .../Webhooks/Guards/GuardWebhook.cs | 24 +- .../Apps/AppCommandMiddlewareTests.cs | 243 +++++++++++++++ .../Apps/AppDomainObjectTests.cs | 288 ++++++++++++++++++ .../Apps/AppEventTests.cs | 51 ++++ .../Apps/Guards/GuardAppContributorsTests.cs | 179 +++++++++++ .../Apps/Guards/GuardAppLanguagesTests.cs | 129 ++++++++ .../Apps/Guards/GuardAppTests.cs | 119 ++++++++ .../Assets/AssetCommandMiddlewareTests.cs | 139 +++++++++ .../Assets/AssetDomainObjectTests.cs | 213 +++++++++++++ .../Assets/Guards/GuardAssetTests.cs | 65 ++++ .../Contents/ContentCommandMiddlewareTests.cs | 249 +++++++++++++++ .../Contents/ContentDomainObjectTests.cs | 280 +++++++++++++++++ .../Contents/ContentEventTests.cs | 70 +++++ .../Contents/ContentVersionLoaderTests.cs | 139 +++++++++ .../JsonFieldPropertiesTests.cs | 27 ++ .../NumberFieldPropertiesTests.cs | 1 - .../StringFieldPropertiesTests.cs | 1 - .../Schemas/Guards/GuardSchemaTests.cs | 20 +- .../Webhooks/Guards/GuardWebhookTests.cs | 138 +++++++++ .../Webhooks/WebhookCommandMiddlewareTests.cs | 115 +++++++ .../Webhooks/WebhookDomainObjectTests.cs | 159 ++++++++++ 43 files changed, 3049 insertions(+), 296 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs create mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs create mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs create mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs create mode 100644 src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs new file mode 100644 index 000000000..83c9506b7 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// AppPlan.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPlan + { + public RefToken Owner { get; } + + public string PlanId { get; } + + public AppPlan(RefToken owner, string planId) + { + Guard.NotNull(owner, nameof(owner)); + Guard.NotNullOrEmpty(planId, nameof(planId)); + + Owner = owner; + + PlanId = planId; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs index ea84163be..327761d38 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs @@ -8,9 +8,9 @@ using System; using System.Threading.Tasks; -using Squidex.Domain.Apps.Read.Apps.Repositories; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Write.Apps.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; @@ -21,118 +21,68 @@ namespace Squidex.Domain.Apps.Write.Apps public class AppCommandMiddleware : ICommandMiddleware { private readonly IAggregateHandler handler; - private readonly IAppRepository appRepository; + private readonly IAppProvider appProvider; private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlanBillingManager appPlansBillingManager; private readonly IUserResolver userResolver; public AppCommandMiddleware( IAggregateHandler handler, - IAppRepository appRepository, + IAppProvider appProvider, IAppPlansProvider appPlansProvider, IAppPlanBillingManager appPlansBillingManager, IUserResolver userResolver) { Guard.NotNull(handler, nameof(handler)); - Guard.NotNull(appRepository, nameof(appRepository)); + 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.appRepository = appRepository; + this.appProvider = appProvider; this.appPlansProvider = appPlansProvider; this.appPlansBillingManager = appPlansBillingManager; } protected async Task On(CreateApp command, CommandContext context) { - if (await appRepository.FindAppAsync(command.Name) != null) + await handler.CreateAsync(context, async a => { - var error = - new ValidationError($"An app with name '{command.Name}' already exists", - nameof(CreateApp.Name)); + await GuardApp.CanCreate(command, appProvider); - throw new ValidationException("Cannot create a new app.", error); - } - - await handler.CreateAsync(context, a => - { a.Create(command); context.Complete(EntityCreatedResult.Create(a.Id, a.Version)); }); } - protected async Task On(AssignContributor command, CommandContext context) + protected Task On(AttachClient command, CommandContext context) { - if (await userResolver.FindByIdAsync(command.ContributorId) == null) - { - var error = - new ValidationError("Cannot find contributor the contributor.", - nameof(AssignContributor.ContributorId)); - - throw new ValidationException("Cannot assign contributor to app.", error); - } + return handler.UpdateAsync(context, a => a.AttachClient(command)); + } - await handler.UpdateAsync(context, a => + protected async Task On(AssignContributor command, CommandContext context) + { + await handler.UpdateAsync(context, async a => { - var oldContributors = a.ContributorCount; - var maxContributors = appPlansProvider.GetPlan(a.PlanId).MaxContributors; + await GuardAppContributors.CanAssign(a.Contributors, command, userResolver, appPlansProvider.GetPlan(a.Plan?.PlanId)); a.AssignContributor(command); - - if (maxContributors > 0 && a.ContributorCount > oldContributors && a.ContributorCount > maxContributors) - { - var error = new ValidationError("You have reached your max number of contributors."); - - throw new ValidationException("Cannot assign contributor to app.", error); - } }); } - protected Task On(ChangePlan command, CommandContext context) + protected Task On(RemoveContributor command, CommandContext context) { - if (!appPlansProvider.IsConfiguredPlan(command.PlanId)) + return handler.UpdateAsync(context, a => { - var error = - new ValidationError($"The plan '{command.PlanId}' does not exists", - nameof(CreateApp.Name)); - - throw new ValidationException("Cannot change plan.", error); - } + GuardAppContributors.CanRemove(a.Contributors, command); - return handler.UpdateAsync(context, async a => - { - if (command.FromCallback) - { - a.ChangePlan(command); - } - else - { - var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId); - - if (result is PlanChangedResult) - { - a.ChangePlan(command); - } - - context.Complete(result); - } + a.RemoveContributor(command); }); } - protected Task On(AttachClient command, CommandContext context) - { - return handler.UpdateAsync(context, a => a.AttachClient(command)); - } - - protected Task On(RemoveContributor command, CommandContext context) - { - return handler.UpdateAsync(context, a => a.RemoveContributor(command)); - } - protected Task On(UpdateClient command, CommandContext context) { return handler.UpdateAsync(context, a => a.UpdateClient(command)); @@ -145,17 +95,56 @@ namespace Squidex.Domain.Apps.Write.Apps protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => a.AddLanguage(command)); + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanAdd(a.LanguagesConfig, command); + + a.AddLanguage(command); + }); } protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => a.RemoveLanguage(command)); + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanRemove(a.LanguagesConfig, command); + + a.RemoveLanguage(command); + }); } protected Task On(UpdateLanguage command, CommandContext context) { - return handler.UpdateAsync(context, a => a.UpdateLanguage(command)); + return handler.UpdateAsync(context, a => + { + GuardAppLanguages.CanUpdate(a.LanguagesConfig, command); + + a.UpdateLanguage(command); + }); + } + + protected Task On(ChangePlan command, CommandContext context) + { + return handler.UpdateAsync(context, async a => + { + GuardApp.CanChangePlan(command, a.Plan, appPlansProvider); + + if (command.FromCallback) + { + a.ChangePlan(command); + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId); + + if (result is PlanChangedResult) + { + a.ChangePlan(command); + } + + context.Complete(result); + } + }); } public async Task HandleAsync(CommandContext context, Func next) diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs index 46a5b486c..ac265d97e 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs @@ -27,23 +27,32 @@ namespace Squidex.Domain.Apps.Write.Apps private readonly AppContributors contributors = new AppContributors(); private readonly AppClients clients = new AppClients(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(DefaultLanguage); + private AppPlan plan; private string name; - private string planId; - private RefToken planOwner; public string Name { get { return name; } } - public string PlanId + public AppPlan Plan { - get { return planId; } + get { return plan; } } - public int ContributorCount + public AppClients Clients { - get { return contributors.Contributors.Count; } + get { return clients; } + } + + public AppContributors Contributors + { + get { return contributors; } + } + + public LanguagesConfig LanguagesConfig + { + get { return languagesConfig; } } public AppDomainObject(Guid id, int version) @@ -103,9 +112,7 @@ namespace Squidex.Domain.Apps.Write.Apps protected void On(AppPlanChanged @event) { - planId = @event.PlanId; - - planOwner = string.IsNullOrWhiteSpace(planId) ? null : @event.Actor; + plan = string.IsNullOrWhiteSpace(@event.PlanId) ? null : new AppPlan(@event.Actor, @event.PlanId); } protected override void DispatchEvent(Envelope @event) @@ -115,8 +122,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject Create(CreateApp command) { - Guard.Valid(command, nameof(command), () => "Cannot create app"); - ThrowIfCreated(); var appId = new NamedId(command.AppId, command.Name); @@ -131,8 +136,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject UpdateClient(UpdateClient command) { - Guard.Valid(command, nameof(command), () => "Cannot update client"); - ThrowIfNotCreated(); if (!string.IsNullOrWhiteSpace(command.Name)) @@ -150,8 +153,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject AssignContributor(AssignContributor command) { - Guard.Valid(command, nameof(command), () => "Cannot assign contributor"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); @@ -161,8 +162,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject RemoveContributor(RemoveContributor command) { - Guard.Valid(command, nameof(command), () => "Cannot remove contributor"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppContributorRemoved())); @@ -172,8 +171,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject AttachClient(AttachClient command) { - Guard.Valid(command, nameof(command), () => "Cannot attach client"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppClientAttached())); @@ -183,8 +180,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject RevokeClient(RevokeClient command) { - Guard.Valid(command, nameof(command), () => "Cannot revoke client"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked())); @@ -194,8 +189,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject AddLanguage(AddLanguage command) { - Guard.Valid(command, nameof(command), () => "Cannot add language"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded())); @@ -205,8 +198,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject RemoveLanguage(RemoveLanguage command) { - Guard.Valid(command, nameof(command), () => "Cannot remove language"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppLanguageRemoved())); @@ -216,8 +207,6 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject UpdateLanguage(UpdateLanguage command) { - Guard.Valid(command, nameof(command), () => "Cannot update language"); - ThrowIfNotCreated(); RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); @@ -227,10 +216,7 @@ namespace Squidex.Domain.Apps.Write.Apps public AppDomainObject ChangePlan(ChangePlan command) { - Guard.Valid(command, nameof(command), () => "Cannot change plan"); - ThrowIfNotCreated(); - ThrowIfOtherUser(command); RaiseEvent(SimpleMapper.Map(command, new AppPlanChanged())); @@ -257,19 +243,6 @@ namespace Squidex.Domain.Apps.Write.Apps return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = AppContributorPermission.Owner }; } - private void ThrowIfOtherUser(ChangePlan command) - { - if (!string.IsNullOrWhiteSpace(command.PlanId) && planOwner != null && !planOwner.Equals(command.Actor)) - { - throw new ValidationException("Plan can only be changed from current user."); - } - - if (string.Equals(command.PlanId, planId, StringComparison.OrdinalIgnoreCase)) - { - throw new ValidationException("App has already this plan."); - } - } - private void ThrowIfNotCreated() { if (string.IsNullOrWhiteSpace(name)) diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs index 18c0d26d5..0d6f3396e 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/AddLanguage.cs @@ -6,21 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class AddLanguage : AppAggregateCommand, IValidatable + public sealed class AddLanguage : AppAggregateCommand { public Language Language { get; set; } - - public void Validate(IList errors) - { - if (Language == null) - { - errors.Add(new ValidationError("Language cannot be null.", nameof(Language))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs index d0595d4d8..361ee348f 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/AssignContributor.cs @@ -6,29 +6,14 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class AssignContributor : AppAggregateCommand, IValidatable + public sealed class AssignContributor : AppAggregateCommand { public string ContributorId { get; set; } public AppContributorPermission Permission { get; set; } - - public void Validate(IList errors) - { - if (string.IsNullOrWhiteSpace(ContributorId)) - { - errors.Add(new ValidationError("Contributor id not assigned.", nameof(ContributorId))); - } - - if (!Permission.IsEnumValue()) - { - errors.Add(new ValidationError("Permission is not valid.", nameof(Permission))); - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs index 9008316eb..f69cfb384 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/AttachClient.cs @@ -6,23 +6,14 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class AttachClient : AppAggregateCommand, IValidatable + public sealed class AttachClient : AppAggregateCommand { public string Id { get; set; } public string Secret { get; } = RandomHash.New(); - - public void Validate(IList errors) - { - if (!Id.IsSlug()) - { - errors.Add(new ValidationError("Client id must be a valid slug.", nameof(Id))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs index 33c7dbdcc..a15d68fb9 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs @@ -6,23 +6,14 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class ChangePlan : AppAggregateCommand, IValidatable + public sealed class ChangePlan : AppAggregateCommand { public bool FromCallback { get; set; } public string PlanId { get; set; } - - public void Validate(IList errors) - { - if (string.IsNullOrWhiteSpace(PlanId)) - { - errors.Add(new ValidationError("PlanId is not defined.", nameof(PlanId))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs index e200e2cb6..95ce15743 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/CreateApp.cs @@ -7,13 +7,11 @@ // ========================================================================== using System; -using System.Collections.Generic; -using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class CreateApp : SquidexCommand, IValidatable, IAggregateCommand + public sealed class CreateApp : SquidexCommand, IAggregateCommand { public string Name { get; set; } @@ -28,13 +26,5 @@ namespace Squidex.Domain.Apps.Write.Apps.Commands { AppId = Guid.NewGuid(); } - - public void Validate(IList errors) - { - if (!Name.IsSlug()) - { - errors.Add(new ValidationError("Name must be a valid slug.", nameof(Name))); - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs index 5f51d3481..c579247a3 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveContributor.cs @@ -6,21 +6,10 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using Squidex.Infrastructure; - namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class RemoveContributor : AppAggregateCommand, IValidatable + public sealed class RemoveContributor : AppAggregateCommand { public string ContributorId { get; set; } - - public void Validate(IList errors) - { - if (string.IsNullOrWhiteSpace(ContributorId)) - { - errors.Add(new ValidationError("Contributor id not assigned.", nameof(ContributorId))); - } - } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs index 678489dfc..8a08d3c93 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/RemoveLanguage.cs @@ -6,21 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class RemoveLanguage : AppAggregateCommand, IValidatable + public sealed class RemoveLanguage : AppAggregateCommand { public Language Language { get; set; } - - public void Validate(IList errors) - { - if (Language == null) - { - errors.Add(new ValidationError("Language cannot be null.", nameof(Language))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs index 5e66285ce..68abb555e 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/RevokeClient.cs @@ -6,21 +6,10 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using Squidex.Infrastructure; - namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class RevokeClient : AppAggregateCommand, IValidatable + public sealed class RevokeClient : AppAggregateCommand { public string Id { get; set; } - - public void Validate(IList errors) - { - if (!Id.IsSlug()) - { - errors.Add(new ValidationError("Client id must be a valid slug.", nameof(Id))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs index 0798a9b36..5cbe2496e 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateClient.cs @@ -6,36 +6,16 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Domain.Apps.Core.Apps; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class UpdateClient : AppAggregateCommand, IValidatable + public sealed class UpdateClient : AppAggregateCommand { public string Id { get; set; } public string Name { get; set; } public AppClientPermission? Permission { get; set; } - - public void Validate(IList errors) - { - if (!Id.IsSlug()) - { - errors.Add(new ValidationError("Client id must be a valid slug.", nameof(Id))); - } - - if (string.IsNullOrWhiteSpace(Name) && Permission == null) - { - errors.Add(new ValidationError("Either name or permission must be defined.", nameof(Name), nameof(Permission))); - } - - if (Permission.HasValue && !Permission.Value.IsEnumValue()) - { - errors.Add(new ValidationError("Permission is not valid.", nameof(Permission))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs index 1d0d28f86..22092874f 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/UpdateLanguage.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Write.Apps.Commands { - public sealed class UpdateLanguage : AppAggregateCommand, IValidatable + public sealed class UpdateLanguage : AppAggregateCommand { public Language Language { get; set; } @@ -20,13 +20,5 @@ namespace Squidex.Domain.Apps.Write.Apps.Commands public bool IsMaster { get; set; } public List Fallback { get; set; } - - public void Validate(IList errors) - { - if (Language == null) - { - errors.Add(new ValidationError("Language cannot be null.", nameof(Language))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs new file mode 100644 index 000000000..c87e8ffda --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardApp.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// GuardApp.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public static class GuardApp + { + public static Task CanCreate(CreateApp command, IAppProvider apps) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot create app.", async error => + { + if (await apps.FindAppByNameAsync(command.Name) != null) + { + error(new ValidationError($"An app with name '{command.Name}' already exists", nameof(command.Name))); + } + + if (!command.Name.IsSlug()) + { + error(new ValidationError("Name must be a valid slug.", nameof(command.Name))); + } + }); + } + + public static void CanChangePlan(ChangePlan command, AppPlan plan, IAppPlansProvider appPlans) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot change plan.", error => + { + if (string.IsNullOrWhiteSpace(command.PlanId)) + { + error(new ValidationError("PlanId is not defined.", nameof(command.PlanId))); + } + else if (appPlans.GetPlan(command.PlanId) == null) + { + error(new ValidationError("Plan id not available.", nameof(command.PlanId))); + } + + if (!string.IsNullOrWhiteSpace(command.PlanId) && plan != null && !plan.Owner.Equals(command.Actor)) + { + error(new ValidationError("Plan can only be changed from current user.")); + } + + if (string.Equals(command.PlanId, plan?.PlanId, StringComparison.OrdinalIgnoreCase)) + { + error(new ValidationError("App has already this plan.")); + } + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs new file mode 100644 index 000000000..daee2032b --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// GuardApp.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public static class GuardAppContributors + { + public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan) + { + Guard.NotNull(command, nameof(command)); + + return Validate.It(() => "Cannot assign contributor.", async error => + { + if (!command.Permission.IsEnumValue()) + { + error(new ValidationError("Permission is not valid.", nameof(command.Permission))); + } + + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); + } + else + { + if (await users.FindByIdAsync(command.ContributorId) == null) + { + error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); + } + else if (contributors.Contributors.TryGetValue(command.ContributorId, out var existing)) + { + if (existing == command.Permission) + { + error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); + } + } + else if (plan.MaxContributors == contributors.Contributors.Count) + { + error(new ValidationError("You have reached the maximum number of contributors for your plan.")); + } + } + }); + } + + public static void CanRemove(AppContributors contributors, RemoveContributor command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot remove contributor.", error => + { + if (string.IsNullOrWhiteSpace(command.ContributorId)) + { + error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); + } + + var ownerIds = contributors.Contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); + + if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) + { + error(new ValidationError("Cannot remove the only owner.", nameof(command.ContributorId))); + } + }); + + if (!contributors.Contributors.ContainsKey(command.ContributorId)) + { + throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject)); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs new file mode 100644 index 000000000..a65e5d05b --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// GuardApp.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public static class GuardAppLanguages + { + public static void CanAdd(LanguagesConfig languages, AddLanguage command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add language.", error => + { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + else if (languages.Contains(command.Language)) + { + error(new ValidationError("Language already added.", nameof(command.Language))); + } + }); + } + + public static void CanRemove(LanguagesConfig languages, RemoveLanguage command) + { + Guard.NotNull(command, nameof(command)); + + var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot remove language.", error => + { + if (languages.Master == languageConfig) + { + error(new ValidationError("Language config is master.", nameof(command.Language))); + } + }); + } + + public static void CanUpdate(LanguagesConfig languages, UpdateLanguage command) + { + Guard.NotNull(command, nameof(command)); + + var languageConfig = GetLanguageConfigOrThrow(languages, command.Language); + + Validate.It(() => "Cannot update language.", error => + { + if ((languages.Master == languageConfig || command.IsMaster) && command.IsOptional) + { + error(new ValidationError("Cannot make master language optional.", nameof(command.IsMaster))); + } + + if (command.Fallback != null) + { + foreach (var fallback in command.Fallback) + { + if (!languages.Contains(fallback)) + { + error(new ValidationError($"Config does not contain fallback language {fallback}.", nameof(command.Fallback))); + } + } + } + }); + } + + private static LanguageConfig GetLanguageConfigOrThrow(LanguagesConfig languages, Language language) + { + if (language == null) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); + } + + if (!languages.TryGetConfig(language, out var languageConfig)) + { + throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); + } + + return languageConfig; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs index 5af329ee8..ae504f17a 100644 --- a/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Assets/AssetCommandMiddleware.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Write.Assets.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.CQRS.Commands; @@ -43,6 +44,8 @@ namespace Squidex.Domain.Apps.Write.Assets { var asset = await handler.CreateAsync(context, async a => { + GuardAsset.CanCreate(command); + a.Create(command); await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); @@ -66,6 +69,8 @@ namespace Squidex.Domain.Apps.Write.Assets { var asset = await handler.UpdateAsync(context, async a => { + GuardAsset.CanUpdate(command); + a.Update(command); await assetStore.UploadTemporaryAsync(context.ContextId.ToString(), command.File.OpenRead()); @@ -83,12 +88,22 @@ namespace Squidex.Domain.Apps.Write.Assets protected Task On(RenameAsset command, CommandContext context) { - return handler.UpdateAsync(context, a => a.Rename(command)); + return handler.UpdateAsync(context, a => + { + GuardAsset.CanRename(command, a.FileName); + + a.Rename(command); + }); } protected Task On(DeleteAsset command, CommandContext context) { - return handler.UpdateAsync(context, a => a.Delete(command)); + return handler.UpdateAsync(context, a => + { + GuardAsset.CanDelete(command); + + a.Delete(command); + }); } public async Task HandleAsync(CommandContext context, Func next) diff --git a/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs index 6060c6826..e3da95a1a 100644 --- a/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Assets/AssetDomainObject.cs @@ -34,6 +34,11 @@ namespace Squidex.Domain.Apps.Write.Assets get { return fileVersion; } } + public string FileName + { + get { return fileName; } + } + public AssetDomainObject(Guid id, int version) : base(id, version) { @@ -66,8 +71,6 @@ namespace Squidex.Domain.Apps.Write.Assets public AssetDomainObject Create(CreateAsset command) { - Guard.NotNull(command, nameof(command)); - VerifyNotCreated(); var @event = SimpleMapper.Map(command, new AssetCreated @@ -88,8 +91,6 @@ namespace Squidex.Domain.Apps.Write.Assets public AssetDomainObject Update(UpdateAsset command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); var @event = SimpleMapper.Map(command, new AssetUpdated @@ -109,8 +110,6 @@ namespace Squidex.Domain.Apps.Write.Assets public AssetDomainObject Delete(DeleteAsset command) { - Guard.NotNull(command, nameof(command)); - VerifyCreatedAndNotDeleted(); RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = totalSize })); @@ -120,24 +119,13 @@ namespace Squidex.Domain.Apps.Write.Assets public AssetDomainObject Rename(RenameAsset command) { - Guard.Valid(command, nameof(command), () => "Cannot rename asset."); - VerifyCreatedAndNotDeleted(); - VerifyDifferentNames(command.FileName, () => "Cannot rename asset."); RaiseEvent(SimpleMapper.Map(command, new AssetRenamed())); return this; } - private void VerifyDifferentNames(string newName, Func message) - { - if (string.Equals(fileName, newName)) - { - throw new ValidationException(message(), new ValidationError("The asset already has this name.", "Name")); - } - } - private void VerifyNotCreated() { if (!string.IsNullOrWhiteSpace(fileName)) diff --git a/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs index 310b7935a..493acc979 100644 --- a/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs +++ b/src/Squidex.Domain.Apps.Write/Assets/Commands/RenameAsset.cs @@ -6,21 +6,10 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using Squidex.Infrastructure; - namespace Squidex.Domain.Apps.Write.Assets.Commands { - public sealed class RenameAsset : AssetAggregateCommand, IValidatable + public sealed class RenameAsset : AssetAggregateCommand { public string FileName { get; set; } - - public void Validate(IList errors) - { - if (string.IsNullOrWhiteSpace(FileName)) - { - errors.Add(new ValidationError("File name must not be null or empty.", nameof(FileName))); - } - } } } diff --git a/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs new file mode 100644 index 000000000..1e3a7a57d --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Assets/Guards/GuardAsset.cs @@ -0,0 +1,49 @@ +// ========================================================================== +// GuardAsset.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Assets.Guards +{ + public static class GuardAsset + { + public static void CanRename(RenameAsset command, string oldName) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot rename asset.", error => + { + if (string.IsNullOrWhiteSpace(command.FileName)) + { + error(new ValidationError("Name must be defined.", nameof(command.FileName))); + } + + if (string.Equals(command.FileName, oldName)) + { + error(new ValidationError("Name is equal to old name.", nameof(command.FileName))); + } + }); + } + + public static void CanCreate(CreateAsset command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanUpdate(UpdateAsset command) + { + Guard.NotNull(command, nameof(command)); + } + + public static void CanDelete(DeleteAsset command) + { + Guard.NotNull(command, nameof(command)); + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs index b02f32d45..67a9eeb77 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchemaField.cs @@ -151,7 +151,7 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards { if (!schema.FieldsById.TryGetValue(fieldId, out var field)) { - throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Field)); + throw new DomainObjectNotFoundException(fieldId.ToString(), "Fields", typeof(Schema)); } return field; diff --git a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs index 01cd3c162..877261b8a 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/SchemaDomainObject.cs @@ -276,10 +276,6 @@ namespace Squidex.Domain.Apps.Write.Schemas { @event.FieldId = new NamedId(field.Id, field.Name); } - else - { - throw new DomainObjectNotFoundException(fieldCommand.FieldId.ToString(), "Fields", typeof(Field)); - } RaiseEvent(@event); } diff --git a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs index f8a784cb1..13359a531 100644 --- a/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs +++ b/src/Squidex.Domain.Apps.Write/Webhooks/Guards/GuardWebhook.cs @@ -43,20 +43,18 @@ namespace Squidex.Domain.Apps.Write.Webhooks.Guards error(new ValidationError("Url must be specified and absolute.", nameof(command.Url))); } - if (command.Schemas == null) + if (command.Schemas != null) { - error(new ValidationError("Schemas cannot be null.", nameof(command.Schemas))); - } - - var schemaErrors = await Task.WhenAll( - command.Schemas.Select(async s => - await schemas.FindSchemaByIdAsync(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) - : null)); - - foreach (var schemaError in schemaErrors.Where(x => x != null)) - { - error(schemaError); + var schemaErrors = await Task.WhenAll( + command.Schemas.Select(async s => + await schemas.FindSchemaByIdAsync(s.SchemaId) == null + ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(command.Schemas)) + : null)); + + foreach (var schemaError in schemaErrors.Where(x => x != null)) + { + error(schemaError); + } } } } diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs new file mode 100644 index 000000000..fcbaa5d40 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandMiddlewareTests.cs @@ -0,0 +1,243 @@ +// ========================================================================== +// AppCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Read.Apps.Services.Implementations; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps +{ + public class AppCommandMiddlewareTests : HandlerTestBase + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppPlansProvider appPlansProvider = A.Fake(); + private readonly IAppPlanBillingManager appPlansBillingManager = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly AppCommandMiddleware sut; + private readonly AppDomainObject app; + private readonly Language language = Language.DE; + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientName = "client"; + + public AppCommandMiddlewareTests() + { + app = new AppDomainObject(AppId, -1); + + A.CallTo(() => appProvider.FindAppByNameAsync(AppName)) + .Returns((IAppEntity)null); + + A.CallTo(() => userResolver.FindByIdAsync(contributorId)) + .Returns(A.Fake()); + + sut = new AppCommandMiddleware(Handler, appProvider, appPlansProvider, appPlansBillingManager, userResolver); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateApp { Name = AppName, AppId = AppId }); + + await TestCreate(app, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(AppId, context.Result>().IdOrValue); + } + + [Fact] + public async Task AssignContributor_should_assign_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, app.Id, app.Name, "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, app.Id, app.Name, "my-plan")) + .Returns(CreateRedirectResult()); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Null(app.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, app.Id, app.Name, "my-plan")).MustNotHaveHappened(); + } + + [Fact] + public async Task AddLanguage_should_update_domain_object() + { + CreateApp(); + + var context = CreateContextForCommand(new AddLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task RemoveLanguage_should_update_domain_object() + { + CreateApp() + .AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + var context = CreateContextForCommand(new RemoveLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + [Fact] + public async Task UpdateLanguage_should_update_domain_object() + { + CreateApp() + .AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + var context = CreateContextForCommand(new UpdateLanguage { Language = language }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + } + + private AppDomainObject CreateApp() + { + app.Create(CreateCommand(new CreateApp { Name = AppName })); + + return app; + } + + private static Task CreateRedirectResult() + { + return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs new file mode 100644 index 000000000..79cdb70b7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppDomainObjectTests.cs @@ -0,0 +1,288 @@ +// ========================================================================== +// AppDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps +{ + public class AppDomainObjectTests : HandlerTestBase + { + private readonly AppDomainObject sut; + private readonly string contributorId = Guid.NewGuid().ToString(); + private readonly string clientId = "client"; + private readonly string clientNewName = "My Client"; + private readonly string planId = "premium"; + + public AppDomainObjectTests() + { + sut = new AppDomainObject(AppId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + CreateApp(); + + Assert.Throws(() => + { + sut.Create(CreateCommand(new CreateApp { Name = AppName })); + }); + } + + [Fact] + public void Create_should_specify_name_and_owner() + { + sut.Create(CreateCommand(new CreateApp { Name = AppName, Actor = User, AppId = AppId })); + + Assert.Equal(AppName, sut.Name); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppCreated { Name = AppName }), + CreateEvent(new AppContributorAssigned { ContributorId = User.Identifier, Permission = AppContributorPermission.Owner }), + CreateEvent(new AppLanguageAdded { Language = Language.EN }) + ); + } + + [Fact] + public void ChangePlan_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); + }); + } + + [Fact] + public void ChangePlan_should_create_events() + { + CreateApp(); + + sut.ChangePlan(CreateCommand(new ChangePlan { PlanId = planId })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppPlanChanged { PlanId = planId }) + ); + } + + [Fact] + public void AssignContributor_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId })); + }); + } + + [Fact] + public void AssignContributor_should_create_events() + { + CreateApp(); + + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Permission = AppContributorPermission.Editor }) + ); + } + + [Fact] + public void RemoveContributor_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); + }); + } + + [Fact] + public void RemoveContributor_should_create_events_and_remove_contributor() + { + CreateApp(); + + sut.AssignContributor(CreateCommand(new AssignContributor { ContributorId = contributorId, Permission = AppContributorPermission.Editor })); + sut.RemoveContributor(CreateCommand(new RemoveContributor { ContributorId = contributorId })); + + sut.GetUncomittedEvents().Skip(1) + .ShouldHaveSameEvents( + CreateEvent(new AppContributorRemoved { ContributorId = contributorId }) + ); + } + + [Fact] + public void AttachClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); + }); + } + + [Fact] + public void AttachClient_should_create_events() + { + var command = new AttachClient { Id = clientId }; + + CreateApp(); + + sut.AttachClient(CreateCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientAttached { Id = clientId, Secret = command.Secret }) + ); + } + + [Fact] + public void RevokeClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RevokeClient(CreateCommand(new RevokeClient { Id = "not-found" })); + }); + } + + [Fact] + public void RevokeClient_should_create_events() + { + CreateApp(); + CreateClient(); + + sut.RevokeClient(CreateCommand(new RevokeClient { Id = clientId })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientRevoked { Id = clientId }) + ); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.UpdateClient(CreateCommand(new UpdateClient { Id = "not-found", Name = clientNewName })); + }); + } + + [Fact] + public void UpdateClient_should_create_events() + { + CreateApp(); + CreateClient(); + + sut.UpdateClient(CreateCommand(new UpdateClient { Id = clientId, Name = clientNewName, Permission = AppClientPermission.Developer })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), + CreateEvent(new AppClientUpdated { Id = clientId, Permission = AppClientPermission.Developer }) + ); + } + + [Fact] + public void AddLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + }); + } + + [Fact] + public void AddLanguage_should_create_events() + { + CreateApp(); + + sut.AddLanguage(CreateCommand(new AddLanguage { Language = Language.DE })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageAdded { Language = Language.DE }) + ); + } + + [Fact] + public void RemoveLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.EN })); + }); + } + + [Fact] + public void RemoveLanguage_should_create_events() + { + CreateApp(); + CreateLanguage(Language.DE); + + sut.RemoveLanguage(CreateCommand(new RemoveLanguage { Language = Language.DE })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageRemoved { Language = Language.DE }) + ); + } + + [Fact] + public void UpdateLanguage_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + 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.EN } })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new AppLanguageUpdated { Language = Language.DE, Fallback = new List { Language.EN } }) + ); + } + + private void CreateApp() + { + sut.Create(CreateCommand(new CreateApp { Name = AppName })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void CreateClient() + { + sut.AttachClient(CreateCommand(new AttachClient { Id = clientId })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void CreateLanguage(Language language) + { + sut.AddLanguage(CreateCommand(new AddLanguage { Language = language })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs new file mode 100644 index 000000000..acb390b7e --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppEventTests.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// AppEventTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Domain.Apps.Events.Apps.Old; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Write.Apps +{ + public class AppEventTests + { + private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + + [Fact] + public void Should_migrate_client_changed_as_reader_to_client_updated() + { + var source = CreateEvent(new AppClientChanged { IsReader = true }); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Reader })); + } + + [Fact] + public void Should_migrate_client_changed_as_writer_to_client_updated() + { + var source = CreateEvent(new AppClientChanged { IsReader = false }); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new AppClientUpdated { Permission = AppClientPermission.Editor })); + } + + private T CreateEvent(T contentEvent) where T : AppEvent + { + contentEvent.Actor = actor; + contentEvent.AppId = appId; + + return contentEvent; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs new file mode 100644 index 000000000..f9d007afd --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -0,0 +1,179 @@ +// ========================================================================== +// GuardAppContributorsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public class GuardAppContributorsTests + { + private readonly IUserResolver users = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + + public GuardAppContributorsTests() + { + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(A.Fake()); + + A.CallTo(() => appPlan.MaxContributors) + .Returns(10); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_contributor_id_is_null() + { + var command = new AssignContributor(); + + var contributors = new AppContributors(); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_permission_not_valid() + { + var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; + + var contributors = new AppContributors(); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_user_already_exists_with_same_permission() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Owner); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_user_not_found() + { + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(Task.FromResult(null)); + + var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; + + var contributors = new AppContributors(); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_throw_exception_if_contributor_max_reached() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "3" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Owner); + contributors.Assign("2", AppContributorPermission.Editor); + + return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_user_found() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + return GuardAppContributors.CanAssign(contributors, command, users, appPlan); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_contributor_has_another_permission() + { + var command = new AssignContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Editor); + + return GuardAppContributors.CanAssign(contributors, command, users, appPlan); + } + + [Fact] + public Task CanAssign_should_not_throw_exception_if_contributor_max_reached_but_permission_changed() + { + A.CallTo(() => appPlan.MaxContributors) + .Returns(2); + + var command = new AssignContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Editor); + contributors.Assign("2", AppContributorPermission.Editor); + + return GuardAppContributors.CanAssign(contributors, command, users, appPlan); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_id_is_null() + { + var command = new RemoveContributor(); + + var contributors = new AppContributors(); + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_not_found() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors, command)); + } + + [Fact] + public void CanRemove_should_throw_exception_if_contributor_is_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Owner); + contributors.Assign("2", AppContributorPermission.Editor); + + Assert.Throws(() => GuardAppContributors.CanRemove(contributors, command)); + } + + [Fact] + public void CanRemove_should_not_throw_exception_if_contributor_not_only_owner() + { + var command = new RemoveContributor { ContributorId = "1" }; + + var contributors = new AppContributors(); + + contributors.Assign("1", AppContributorPermission.Owner); + contributors.Assign("2", AppContributorPermission.Owner); + + GuardAppContributors.CanRemove(contributors, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs new file mode 100644 index 000000000..a03087358 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// GuardAppLanguagesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public class GuardAppLanguagesTests + { + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_is_null() + { + var command = new AddLanguage(); + + var languages = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => GuardAppLanguages.CanAdd(languages, command)); + } + + [Fact] + public void CanAddLanguage_should_throw_exception_if_language_already_added() + { + var command = new AddLanguage { Language = Language.DE }; + + var languages = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => GuardAppLanguages.CanAdd(languages, command)); + } + + [Fact] + public void CanAddLanguage_should_not_throw_exception_if_language_valid() + { + var command = new AddLanguage { Language = Language.EN }; + + var languages = LanguagesConfig.Build(Language.DE); + + GuardAppLanguages.CanAdd(languages, command); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_null() + { + var command = new RemoveLanguage(); + + var languages = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_not_found() + { + var command = new RemoveLanguage { Language = Language.EN }; + + var languages = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); + } + + [Fact] + public void CanRemoveLanguage_should_throw_exception_if_language_is_master() + { + var command = new RemoveLanguage { Language = Language.DE }; + + var languages = LanguagesConfig.Build(Language.DE); + + Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); + } + + [Fact] + public void CanRemoveLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new RemoveLanguage { Language = Language.EN }; + + var languages = LanguagesConfig.Build(Language.DE, Language.EN); + + GuardAppLanguages.CanRemove(languages, command); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() + { + var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; + + var languages = LanguagesConfig.Build(Language.DE, Language.EN); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_has_invalid_fallback() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.IT } }; + + var languages = LanguagesConfig.Build(Language.DE, Language.EN); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); + } + + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_not_found() + { + var command = new UpdateLanguage { Language = Language.IT }; + + var languages = LanguagesConfig.Build(Language.DE, Language.EN); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); + } + + [Fact] + public void CanUpdateLanguage_should_not_throw_exception_if_language_is_valid() + { + var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; + + var languages = LanguagesConfig.Build(Language.DE, Language.EN); + + GuardAppLanguages.CanUpdate(languages, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs new file mode 100644 index 000000000..758187e8d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// GuardAppTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public class GuardAppTests + { + private readonly IAppProvider apps = A.Fake(); + private readonly IUserResolver users = A.Fake(); + private readonly IAppPlansProvider appPlans = A.Fake(); + + public GuardAppTests() + { + A.CallTo(() => apps.FindAppByNameAsync("new-app")) + .Returns(Task.FromResult(null)); + + A.CallTo(() => users.FindByIdAsync(A.Ignored)) + .Returns(A.Fake()); + + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(A.Fake()); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_already_in_use() + { + A.CallTo(() => apps.FindAppByNameAsync("new-app")) + .Returns(A.Fake()); + + var command = new CreateApp { Name = "new-app" }; + + return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); + } + + [Fact] + public Task CanCreate_should_throw_exception_if_name_not_valid() + { + var command = new CreateApp { Name = "INVALID NAME" }; + + return Assert.ThrowsAsync(() => GuardApp.CanCreate(command, apps)); + } + + [Fact] + public Task CanCreate_should_not_throw_exception_if_app_name_is_free() + { + var command = new CreateApp { Name = "new-app" }; + + return GuardApp.CanCreate(command, apps); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_id_null() + { + var command = new ChangePlan { Actor = new RefToken("user", "me") }; + + AppPlan plan = null; + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_not_found() + { + A.CallTo(() => appPlans.GetPlan("free")) + .Returns(null); + + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + AppPlan plan = null; + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_was_configured_from_another_user() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "other"), "premium"); + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_throw_exception_if_plan_is_the_same() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "me"), "free"); + + Assert.Throws(() => GuardApp.CanChangePlan(command, plan, appPlans)); + } + + [Fact] + public void CanChangePlan_should_not_throw_exception_if_same_user_but_other_plan() + { + var command = new ChangePlan { PlanId = "free", Actor = new RefToken("user", "me") }; + + var plan = new AppPlan(new RefToken("user", "me"), "premium"); + + GuardApp.CanChangePlan(command, plan, appPlans); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs new file mode 100644 index 000000000..d39249f13 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// AssetCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Assets +{ + public class AssetCommandMiddlewareTests : HandlerTestBase + { + private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); + private readonly IAssetStore assetStore = A.Fake(); + private readonly AssetCommandMiddleware sut; + private readonly AssetDomainObject asset; + private readonly Guid assetId = Guid.NewGuid(); + private readonly Stream stream = new MemoryStream(); + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetFile file; + + public AssetCommandMiddlewareTests() + { + file = new AssetFile("my-image.png", "image/png", 1024, () => stream); + + asset = new AssetDomainObject(assetId, -1); + + sut = new AssetCommandMiddleware(Handler, assetStore, assetThumbnailGenerator); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateAsset { AssetId = assetId, File = file }); + + SetupStore(0, context.ContextId); + SetupImageInfo(); + + await TestCreate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Equal(assetId, context.Result>().IdOrValue); + + VerifyStore(0, context.ContextId); + VerifyImageInfo(); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateAsset { AssetId = assetId, File = file }); + + SetupStore(1, context.ContextId); + SetupImageInfo(); + + CreateAsset(); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(context); + }); + + VerifyStore(1, context.ContextId); + VerifyImageInfo(); + } + + [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() + { + CreateAsset(); + + var command = CreateContextForCommand(new DeleteAsset { AssetId = assetId }); + + await TestUpdate(asset, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void CreateAsset() + { + asset.Create(new CreateAsset { File = file }); + } + + private void SetupImageInfo() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)) + .Returns(image); + } + + private void SetupStore(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)) + .Returns(TaskHelper.Done); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())) + .Returns(TaskHelper.Done); + } + + private void VerifyImageInfo() + { + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream)).MustHaveHappened(); + } + + private void VerifyStore(long version, Guid commitId) + { + A.CallTo(() => assetStore.UploadTemporaryAsync(commitId.ToString(), stream)).MustHaveHappened(); + A.CallTo(() => assetStore.CopyTemporaryAsync(commitId.ToString(), assetId.ToString(), version, null)).MustHaveHappened(); + A.CallTo(() => assetStore.DeleteTemporaryAsync(commitId.ToString())).MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs new file mode 100644 index 000000000..cc2858533 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Assets/AssetDomainObjectTests.cs @@ -0,0 +1,213 @@ +// ========================================================================== +// AssetDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using Squidex.Domain.Apps.Events.Assets; +using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Assets +{ + public class AssetDomainObjectTests : HandlerTestBase + { + private readonly AssetDomainObject sut; + private readonly ImageInfo image = new ImageInfo(2048, 2048); + private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); + + public Guid AssetId { get; } = Guid.NewGuid(); + + public AssetDomainObjectTests() + { + sut = new AssetDomainObject(AssetId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateAsset { File = file }); + + Assert.Throws(() => + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + }); + } + + [Fact] + public void Create_should_create_events() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file, ImageInfo = image })); + + 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(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateAsset(); + + sut.Update(CreateAssetCommand(new UpdateAsset { File = file, ImageInfo = image })); + + 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(() => + { + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "new-file.png" })); + }); + } + + [Fact] + public void Rename_should_throw_exception_if_asset_is_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + sut.Update(CreateAssetCommand(new UpdateAsset())); + }); + } + + [Fact] + public void Rename_should_create_events() + { + CreateAsset(); + + sut.Rename(CreateAssetCommand(new RenameAsset { FileName = "my-new-image.png" })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetRenamed { FileName = "my-new-image.png" }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateAsset(); + DeleteAsset(); + + Assert.Throws(() => + { + 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.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) + ); + } + + private void CreateAsset() + { + sut.Create(CreateAssetCommand(new CreateAsset { File = file })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void UpdateAsset() + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteAsset() + { + sut.Delete(CreateAssetCommand(new DeleteAsset())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateAssetEvent(T @event) where T : AssetEvent + { + @event.AssetId = AssetId; + + return CreateEvent(@event); + } + + protected T CreateAssetCommand(T command) where T : AssetAggregateCommand + { + command.AssetId = AssetId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs new file mode 100644 index 000000000..aa34e4b0b --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Assets/Guards/GuardAssetTests.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// GuardAssetTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Write.Assets.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Assets.Guards +{ + public class GuardAssetTests + { + [Fact] + public void CanRename_should_throw_exception_if_name_not_defined() + { + var command = new RenameAsset(); + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_should_throw_exception_if_name_are_the_same() + { + var command = new RenameAsset { FileName = "asset-name" }; + + Assert.Throws(() => GuardAsset.CanRename(command, "asset-name")); + } + + [Fact] + public void CanRename_not_should_throw_exception_if_name_are_different() + { + var command = new RenameAsset { FileName = "new-name" }; + + GuardAsset.CanRename(command, "asset-name"); + } + + [Fact] + public void CanCreate_should_not_throw_exception() + { + var command = new CreateAsset(); + + GuardAsset.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_not_throw_exception() + { + var command = new UpdateAsset(); + + GuardAsset.CanUpdate(command); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteAsset(); + + GuardAsset.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs new file mode 100644 index 000000000..9d02d19b5 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -0,0 +1,249 @@ +// ========================================================================== +// ContentCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Read.Contents.Repositories; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public class ContentCommandMiddlewareTests : HandlerTestBase + { + private readonly ContentCommandMiddleware sut; + private readonly ContentDomainObject content; + private readonly ISchemaProvider schemas = A.Fake(); + private readonly ISchemaEntity schema = A.Fake(); + private readonly IScriptEngine scriptEngine = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly IAppEntity app = A.Fake(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); + private readonly Guid contentId = Guid.NewGuid(); + + 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"); + + schemaDef.AddField(new NumberField(1, "my-field1", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = true })); + schemaDef.AddField(new NumberField(2, "my-field2", Partitioning.Invariant, + new NumberFieldProperties { IsRequired = false })); + + content = new ContentDomainObject(contentId, -1); + + sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), schemas, scriptEngine, A.Dummy()); + + A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); + A.CallTo(() => app.PartitionResolver).Returns(languagesConfig.ToResolver()); + + A.CallTo(() => appProvider.FindAppByIdAsync(AppId)).Returns(app); + + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.ScriptCreate).Returns(""); + A.CallTo(() => schema.ScriptChange).Returns(""); + A.CallTo(() => schema.ScriptUpdate).Returns(""); + A.CallTo(() => schema.ScriptDelete).Returns(""); + + A.CallTo(() => schemas.FindSchemaByIdAsync(SchemaId, false)).Returns(schema); + } + + [Fact] + public async Task Create_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Create_should_create_content() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustNotHaveHappened(); + } + + [Fact] + public async Task Create_should_also_invoke_publish_script_when_publishing() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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>().IdOrValue); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).MustHaveHappened(); + } + + [Fact] + public async Task Patch_should_throw_exception_if_data_is_not_valid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidData); + + CreateContent(); + + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); + + await TestUpdate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Patch_should_update_domain_object() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.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().Data); + + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, "")).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.Ignored, "")).MustHaveHappened(); + } + + [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.Ignored, "")).MustHaveHappened(); + } + + private void CreateContent() + { + content.Create(new CreateContent { Data = data }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs new file mode 100644 index 000000000..5a8515c19 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentDomainObjectTests.cs @@ -0,0 +1,280 @@ +// ========================================================================== +// ContentDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public class ContentDomainObjectTests : HandlerTestBase + { + private readonly ContentDomainObject sut; + 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)); + + public Guid ContentId { get; } = Guid.NewGuid(); + + public ContentDomainObjectTests() + { + sut = new ContentDomainObject(ContentId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateContent { Data = data }); + + Assert.Throws(() => + { + 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(() => + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + 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(() => + { + sut.Patch(CreateContentCommand(new PatchContent { Data = data })); + }); + } + + [Fact] + public void Patch_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Patch(CreateContentCommand(new PatchContent())); + }); + } + + [Fact] + public void Patch_should_create_events() + { + CreateContent(); + + sut.Patch(CreateContentCommand(new PatchContent { Data = otherData })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = otherData }) + ); + } + + [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(() => + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus())); + }); + } + + [Fact] + public void ChangeStatus_should_throw_exception_if_content_is_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + 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.Status); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentStatusChanged { Status = Status.Published }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateContent(); + DeleteContent(); + + Assert.Throws(() => + { + sut.Delete(CreateContentCommand(new DeleteContent())); + }); + } + + [Fact] + public void Delete_should_update_properties_and_create_events() + { + CreateContent(); + + sut.Delete(CreateContentCommand(new DeleteContent())); + + Assert.True(sut.IsDeleted); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateContentEvent(new ContentDeleted()) + ); + } + + private void CreateContent() + { + sut.Create(CreateContentCommand(new CreateContent { Data = data })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void UpdateContent() + { + sut.Update(CreateContentCommand(new UpdateContent { Data = data })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void ChangeStatus(Status status) + { + sut.ChangeStatus(CreateContentCommand(new ChangeContentStatus { Status = status })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteContent() + { + sut.Delete(CreateContentCommand(new DeleteContent())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateContentEvent(T @event) where T : ContentEvent + { + @event.ContentId = ContentId; + + return CreateEvent(@event); + } + + protected T CreateContentCommand(T command) where T : ContentCommand + { + command.ContentId = ContentId; + + return CreateCommand(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs new file mode 100644 index 000000000..dcdfe4fee --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentEventTests.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// SchemaEventTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Domain.Apps.Events.Contents.Old; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +#pragma warning disable CS0612 // Type or member is obsolete + +namespace Squidex.Domain.Apps.Write.Contents +{ + public class ContentEventTests + { + private readonly RefToken actor = new RefToken("User", Guid.NewGuid().ToString()); + private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); + private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); + private readonly Guid contentId = Guid.NewGuid(); + + [Fact] + public void Should_migrate_content_published_to_content_status_changed() + { + var source = CreateEvent(new ContentPublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Published })); + } + + [Fact] + public void Should_migrate_content_unpublished_to_content_status_changed() + { + var source = CreateEvent(new ContentUnpublished()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_restored_to_content_status_changed() + { + var source = CreateEvent(new ContentRestored()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Draft })); + } + + [Fact] + public void Should_migrate_content_archived_to_content_status_changed() + { + var source = CreateEvent(new ContentArchived()); + + source.Migrate().ShouldBeSameEvent(CreateEvent(new ContentStatusChanged { Status = Status.Archived })); + } + + private T CreateEvent(T contentEvent) where T : ContentEvent + { + contentEvent.Actor = actor; + contentEvent.AppId = appId; + contentEvent.SchemaId = schemaId; + contentEvent.ContentId = contentId; + + return contentEvent; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs new file mode 100644 index 000000000..5a4cf45ed --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentVersionLoaderTests.cs @@ -0,0 +1,139 @@ +// ========================================================================== +// ContentVersionLoaderTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Events.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public class ContentVersionLoaderTests + { + private readonly IEventStore eventStore = A.Fake(); + private readonly IStreamNameResolver nameResolver = A.Fake(); + private readonly EventDataFormatter formatter = A.Fake(); + private readonly Guid id = Guid.NewGuid(); + private readonly Guid appId = Guid.NewGuid(); + private readonly string streamName = Guid.NewGuid().ToString(); + private readonly ContentVersionLoader sut; + + public ContentVersionLoaderTests() + { + A.CallTo(() => nameResolver.GetStreamName(typeof(ContentDomainObject), id)) + .Returns(streamName); + + sut = new ContentVersionLoader(eventStore, nameResolver, formatter); + } + + [Fact] + public async Task Should_throw_exception_when_event_store_returns_no_events() + { + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(new List()); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, -1)); + } + + [Fact] + public async Task Should_throw_exception_when_version_not_found() + { + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(new List()); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 3)); + } + + [Fact] + public async Task Should_throw_exception_when_content_is_from_another_event() + { + var eventData1 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(Guid.NewGuid(), "my-app") }; + + var events = new List + { + new StoredEvent("0", 0, eventData1) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1, true)) + .Returns(new Envelope(event1)); + + await Assert.ThrowsAsync(() => sut.LoadAsync(appId, id, 0)); + } + + [Fact] + public async Task Should_load_content_from_created_event() + { + var eventData1 = new EventData(); + var eventData2 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; + var event2 = new ContentStatusChanged(); + + var events = new List + { + new StoredEvent("0", 0, eventData1), + new StoredEvent("1", 1, eventData2) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1, true)) + .Returns(new Envelope(event1)); + A.CallTo(() => formatter.Parse(eventData2, true)) + .Returns(new Envelope(event2)); + + var data = await sut.LoadAsync(appId, id, 3); + + Assert.Same(event1.Data, data); + } + + [Fact] + public async Task Should_load_content_from_correct_version() + { + var eventData1 = new EventData(); + var eventData2 = new EventData(); + var eventData3 = new EventData(); + + var event1 = new ContentCreated { Data = new NamedContentData(), AppId = new NamedId(appId, "my-app") }; + var event2 = new ContentUpdated { Data = new NamedContentData() }; + var event3 = new ContentUpdated { Data = new NamedContentData() }; + + var events = new List + { + new StoredEvent("0", 0, eventData1), + new StoredEvent("1", 1, eventData2), + new StoredEvent("2", 2, eventData3) + }; + + A.CallTo(() => eventStore.GetEventsAsync(streamName)) + .Returns(events); + + A.CallTo(() => formatter.Parse(eventData1, true)) + .Returns(new Envelope(event1)); + A.CallTo(() => formatter.Parse(eventData2, true)) + .Returns(new Envelope(event2)); + A.CallTo(() => formatter.Parse(eventData3, true)) + .Returns(new Envelope(event3)); + + var data = await sut.LoadAsync(appId, id, 1); + + Assert.Equal(event2.Data, data); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs new file mode 100644 index 000000000..c4415bf1d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/JsonFieldPropertiesTests.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// JsonFieldPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Schemas.Guards.FieldProperties +{ + public class JsonFieldPropertiesTests + { + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new JsonFieldProperties(); + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs index 4e299748c..9c5fa36ad 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs index 5dba533d7..2b6ca22be 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs index 901d61b67..300ed2e4e 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -36,26 +36,26 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards } [Fact] - public async Task CanCreate_should_throw_exception_if_name_not_valid() + public Task CanCreate_should_throw_exception_if_name_not_valid() { var command = new CreateSchema { AppId = appId, Name = "INVALID NAME" }; - await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); } [Fact] - public async Task CanCreate_should_throw_exception_if_name_already_in_use() + public Task CanCreate_should_throw_exception_if_name_already_in_use() { A.CallTo(() => schemas.FindSchemaByNameAsync(A.Ignored, "new-schema")) .Returns(Task.FromResult(A.Fake())); var command = new CreateSchema { AppId = appId, Name = "new-schema" }; - await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); } [Fact] - public async Task CanCreate_should_throw_exception_if_fields_not_valid() + public Task CanCreate_should_throw_exception_if_fields_not_valid() { var command = new CreateSchema { @@ -78,11 +78,11 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards Name = "new-schema" }; - await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); } [Fact] - public async Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names() + public Task CanCreate_should_throw_exception_if_fields_contain_duplicate_names() { var command = new CreateSchema { @@ -105,15 +105,15 @@ namespace Squidex.Domain.Apps.Write.Schemas.Guards Name = "new-schema" }; - await Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); + return Assert.ThrowsAsync(() => GuardSchema.CanCreate(command, schemas)); } [Fact] - public async Task CanCreate_should_not_throw_exception_if_command_is_valid() + public Task CanCreate_should_not_throw_exception_if_command_is_valid() { var command = new CreateSchema { AppId = appId, Name = "new-schema" }; - await GuardSchema.CanCreate(command, schemas); + return GuardSchema.CanCreate(command, schemas); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs new file mode 100644 index 000000000..567e4a241 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/Guards/GuardWebhookTests.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// GuardWebhookTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Webhooks; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Webhooks.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Webhooks.Guards +{ + public class GuardWebhookTests + { + private readonly ISchemaProvider schemas = A.Fake(); + + public GuardWebhookTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(A.Fake()); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_url_defined() + { + var command = new CreateWebhook(); + + await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_url_not_valid() + { + var command = new CreateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_throw_exception_if_schema_id_not_found() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(Task.FromResult(null)); + + var command = new CreateWebhook + { + Schemas = new List + { + new WebhookSchema() + }, + Url = new Uri("/invalid", UriKind.Relative) + }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanCreate_should_not_throw_exception_if_schema_id_found() + { + var command = new CreateWebhook + { + Schemas = new List + { + new WebhookSchema() + }, + Url = new Uri("/invalid", UriKind.Relative) + }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanCreate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_url_not_defined() + { + var command = new UpdateWebhook(); + + await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_url_not_valid() + { + var command = new UpdateWebhook { Url = new Uri("/invalid", UriKind.Relative) }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_throw_exception_if_schema_id_not_found() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(A.Ignored, false)) + .Returns(Task.FromResult(null)); + + var command = new UpdateWebhook + { + Schemas = new List + { + new WebhookSchema() + }, + Url = new Uri("/invalid", UriKind.Relative) + }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); + } + + [Fact] + public async Task CanUpdate_should_not_throw_exception_if_schema_id_found() + { + var command = new UpdateWebhook + { + Schemas = new List + { + new WebhookSchema() + }, + Url = new Uri("/invalid", UriKind.Relative) + }; + + await Assert.ThrowsAsync(() => GuardWebhook.CanUpdate(command, schemas)); + } + + [Fact] + public void CanDelete_should_not_throw_exception() + { + var command = new DeleteWebhook(); + + GuardWebhook.CanDelete(command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs new file mode 100644 index 000000000..37e786b7d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookCommandMiddlewareTests.cs @@ -0,0 +1,115 @@ +// ========================================================================== +// WebhookCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Webhooks; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Domain.Apps.Write.Webhooks.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Webhooks +{ + public class WebhookCommandMiddlewareTests : HandlerTestBase + { + private readonly ISchemaProvider schemas = A.Fake(); + private readonly WebhookCommandMiddleware sut; + private readonly WebhookDomainObject webhook; + private readonly Uri url = new Uri("http://squidex.io"); + private readonly Guid schemaId = Guid.NewGuid(); + private readonly Guid webhookId = Guid.NewGuid(); + private readonly List webhookSchemas; + + public WebhookCommandMiddlewareTests() + { + A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)) + .Returns(A.Fake()); + + webhook = new WebhookDomainObject(webhookId, -1); + + webhookSchemas = new List + { + new WebhookSchema { SchemaId = schemaId } + }; + + sut = new WebhookCommandMiddleware(Handler, schemas); + } + + [Fact] + public async Task Create_should_create_domain_object() + { + var context = CreateContextForCommand(new CreateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); + + await TestCreate(webhook, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); + } + + [Fact] + public async Task Update_should_update_domain_object() + { + var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); + + A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns(A.Fake()); + + CreateWebhook(); + + await TestUpdate(webhook, async _ => + { + await sut.HandleAsync(context); + }); + + A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).MustHaveHappened(); + } + + [Fact] + public async Task Update_should_throw_exception_when_schema_is_not_found() + { + var context = CreateContextForCommand(new UpdateWebhook { Schemas = webhookSchemas, Url = url, WebhookId = webhookId }); + + A.CallTo(() => schemas.FindSchemaByIdAsync(schemaId, false)).Returns((ISchemaEntity)null); + + CreateWebhook(); + + await Assert.ThrowsAsync(async () => + { + await TestCreate(webhook, async _ => + { + await sut.HandleAsync(context); + }); + }); + } + + [Fact] + public async Task Delete_should_update_domain_object() + { + CreateWebhook(); + + var command = CreateContextForCommand(new DeleteWebhook { WebhookId = webhookId }); + + await TestUpdate(webhook, async _ => + { + await sut.HandleAsync(command); + }); + } + + private void CreateWebhook() + { + webhook.Create(new CreateWebhook { Url = url }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs new file mode 100644 index 000000000..182f00d84 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Webhooks/WebhookDomainObjectTests.cs @@ -0,0 +1,159 @@ +// ========================================================================== +// WebhookDomainObjectTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Events.Webhooks; +using Squidex.Domain.Apps.Write.TestHelpers; +using Squidex.Domain.Apps.Write.Webhooks.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Webhooks +{ + public class WebhookDomainObjectTests : HandlerTestBase + { + private readonly Uri url = new Uri("http://squidex.io"); + private readonly WebhookDomainObject sut; + + public Guid WebhookId { get; } = Guid.NewGuid(); + + public WebhookDomainObjectTests() + { + sut = new WebhookDomainObject(WebhookId, 0); + } + + [Fact] + public void Create_should_throw_exception_if_created() + { + sut.Create(new CreateWebhook { Url = url }); + + Assert.Throws(() => + { + sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); + }); + } + + [Fact] + public void Create_should_create_events() + { + var command = new CreateWebhook { Url = url }; + + sut.Create(CreateWebhookCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateWebhookEvent(new WebhookCreated + { + Url = url, + Schemas = command.Schemas, + SharedSecret = command.SharedSecret, + WebhookId = command.WebhookId + }) + ); + } + + [Fact] + public void Update_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); + }); + } + + [Fact] + public void Update_should_throw_exception_if_webhook_is_deleted() + { + CreateWebhook(); + DeleteWebhook(); + + Assert.Throws(() => + { + sut.Update(CreateWebhookCommand(new UpdateWebhook { Url = url })); + }); + } + + [Fact] + public void Update_should_create_events() + { + CreateWebhook(); + + var command = new UpdateWebhook { Url = url }; + + sut.Update(CreateWebhookCommand(command)); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateWebhookEvent(new WebhookUpdated { Url = url, Schemas = command.Schemas }) + ); + } + + [Fact] + public void Delete_should_throw_exception_if_not_created() + { + Assert.Throws(() => + { + sut.Delete(CreateWebhookCommand(new DeleteWebhook())); + }); + } + + [Fact] + public void Delete_should_throw_exception_if_already_deleted() + { + CreateWebhook(); + DeleteWebhook(); + + Assert.Throws(() => + { + sut.Delete(CreateWebhookCommand(new DeleteWebhook())); + }); + } + + [Fact] + public void Delete_should_update_properties_create_events() + { + CreateWebhook(); + + sut.Delete(CreateWebhookCommand(new DeleteWebhook())); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateWebhookEvent(new WebhookDeleted()) + ); + } + + private void CreateWebhook() + { + sut.Create(CreateWebhookCommand(new CreateWebhook { Url = url })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteWebhook() + { + sut.Delete(CreateWebhookCommand(new DeleteWebhook())); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + protected T CreateWebhookEvent(T @event) where T : WebhookEvent + { + @event.WebhookId = WebhookId; + + return CreateEvent(@event); + } + + protected T CreateWebhookCommand(T command) where T : WebhookAggregateCommand + { + command.WebhookId = WebhookId; + + return CreateCommand(command); + } + } +} From 2b0681ef6791cef78a3e61c11b521cec833b95a6 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 16:11:18 +0200 Subject: [PATCH 05/10] Json support --- Squidex.sln.DotSettings | 1 + .../Apps/AppClient.cs | 20 ++- .../Apps/AppClients.cs | 16 +- .../Apps/AppContributors.cs | 14 +- .../Apps/Json/AppClientsConverter.cs | 50 +++++++ .../Apps/Json/AppContributorsConverter.cs | 50 +++++++ .../Apps/Json/JsonAppClient.cs | 39 +++++ .../Apps/Json/JsonLanguageConfig.cs | 40 +++++ .../Apps/Json/LanguagesConfigConverter.cs | 52 +++++++ .../{ => Apps}/LanguageConfig.cs | 2 +- .../{ => Apps}/LanguagesConfig.cs | 36 +++-- .../DictionaryBase.cs | 63 ++++++++ .../Schemas/FieldProperties.cs | 2 - .../Schemas/NamedElementPropertiesBase.cs | 2 - .../ConvertContent/ContentConverter.cs | 1 + .../EnrichContent/ContentEnricher.cs | 1 - .../EnrichContent/DefaultValueFactory.cs | 1 - .../Apps/Utils/AppEventDispatcher.cs | 5 +- .../Apps/MongoAppEntity.cs | 6 +- .../Apps/MongoAppRepository.cs | 5 +- .../Apps/MongoAppRepository_EventHandling.cs | 2 +- .../Contents/Extensions.cs | 16 +- .../Contents/MongoContentEntity.cs | 14 +- .../Contents/MongoContentRepository.cs | 12 +- .../MongoContentRepository_EventHandling.cs | 6 +- .../Schemas/MongoSchemaEntity.cs | 2 +- .../Schemas/MongoSchemaRepository.cs | 6 +- .../Apps/IAppClientEntity.cs | 21 --- .../Apps/AppCommandMiddleware.cs | 29 +++- .../Apps/AppDomainObject.cs | 1 - .../Apps/Commands/ChangePlan.cs | 2 - .../Apps/Guards/GuardAppClients.cs | 102 +++++++++++++ .../Apps/Guards/GuardAppContributors.cs | 8 +- .../Apps/Guards/GuardAppLanguages.cs | 14 +- .../Contents/Commands/PatchContent.cs | 2 - .../Contents/Commands/UpdateContent.cs | 2 - .../Contents/ContentCommandMiddleware.cs | 111 +++++--------- .../Contents/ContentOperationContext.cs | 134 +++++++++++++++++ .../Contents/Guards/GuardContent.cs | 11 +- .../Schemas/Guards/GuardSchema.cs | 1 - .../UserClaimsPrincipalFactoryWithEmail.cs | 1 - .../MongoDb/BsonJsonAttribute.cs} | 9 +- .../MongoDb/BsonJsonConvention.cs | 35 +++++ .../CollectionExtensions.cs | 16 ++ .../DictionaryWrapper.cs | 84 ----------- src/Squidex/Config/Domain/Serializers.cs | 7 + .../Config/Identity/LazyClientStore.cs | 4 +- .../Api/Apps/AppContributorsController.cs | 2 +- .../Api/Apps/AppLanguagesController.cs | 2 +- .../Controllers/Api/Apps/AppsController.cs | 2 +- .../Models/NumberFieldPropertiesDto.cs | 6 - .../Models/StringFieldPropertiesDto.cs | 6 - .../ContentApi/ContentsController.cs | 1 + .../Generator/SchemaSwaggerGenerator.cs | 2 +- .../Pipeline/AppPermissionAttribute.cs | 4 +- src/Squidex/Squidex.csproj | 4 +- .../Model/Apps/AppClientsTests.cs | 103 +++++++++++++ .../Model/Apps/AppContributorsTests.cs | 70 +++++++++ .../Model/Apps/AppPlanTests.cs | 32 ++++ .../Model/Apps/LanguagesConfigJsonTests.cs | 35 +++++ .../Model/{ => Apps}/LanguagesConfigTests.cs | 3 +- .../Model/Contents/ContentDataTests.cs | 17 +++ .../Model/Schemas/Json/JsonSerializerTests.cs | 55 ------- .../Model/Schemas/SchemaFieldTests.cs | 91 ++++++++++++ .../Model/Schemas/SchemaTests.cs | 83 ++--------- .../ConvertContent/ContentConversionTests.cs | 3 +- .../EnrichContent/ContentEnrichmentTests.cs | 1 + .../ReferenceExtractionTests.cs | 1 + .../Operations/GenerateEdmSchema/EdmTests.cs | 1 + .../GenerateJsonSchema/JsonSchemaTests.cs | 1 + .../ValidateContent/ContentValidationTests.cs | 1 + .../ValidateContent/NumberFieldTests.cs | 1 - .../ValidateContent/StringFieldTests.cs | 1 - .../TestData.cs | 33 +++++ .../Contents/GraphQLTests.cs | 50 ++++--- .../Contents/ODataQueryTests.cs | 49 +++--- .../Apps/Guards/GuardAppClientsTests.cs | 140 ++++++++++++++++++ .../Apps/Guards/GuardAppContributorsTests.cs | 25 +--- .../Apps/Guards/GuardAppLanguagesTests.cs | 38 ++--- .../Contents/ContentCommandMiddlewareTests.cs | 1 + .../Contents/Guard/GuardContentTests.cs | 99 +++++++++++++ 81 files changed, 1488 insertions(+), 533 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs rename src/Squidex.Domain.Apps.Core.Model/{ => Apps}/LanguageConfig.cs (97%) rename src/Squidex.Domain.Apps.Core.Model/{ => Apps}/LanguagesConfig.cs (90%) create mode 100644 src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs delete mode 100644 src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs create mode 100644 src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs create mode 100644 src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs rename src/{Squidex.Domain.Apps.Read/Apps/IAppContributorEntity.cs => Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs} (62%) create mode 100644 src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs delete mode 100644 src/Squidex.Infrastructure/DictionaryWrapper.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs rename tests/Squidex.Domain.Apps.Core.Tests/Model/{ => Apps}/LanguagesConfigTests.cs (98%) delete mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs create mode 100644 tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index cf27c2d87..0e0d4202d 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -17,6 +17,7 @@ + <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index fe4fa0bc5..28e17d013 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -16,14 +16,30 @@ namespace Squidex.Domain.Apps.Core.Apps private string name; private AppClientPermission permission; - public AppClient(string name, string secret) + public string Name + { + get { return name; } + } + + public string Secret + { + get { return secret; } + } + + public AppClientPermission Permission + { + get { return permission; } + } + + public AppClient(string name, string secret, AppClientPermission permission) { Guard.NotNullOrEmpty(name, nameof(name)); Guard.NotNullOrEmpty(secret, nameof(secret)); + Guard.Enum(permission, nameof(permission)); this.name = name; - this.secret = secret; + this.permission = permission; } public void Update(AppClientPermission newPermission) diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index 07001417f..a92de925b 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -6,32 +6,32 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { - public class AppClients + public sealed class AppClients : DictionaryBase { - private readonly Dictionary clients = new Dictionary(); - - public IReadOnlyDictionary Clients + public void Add(string id, AppClient client) { - get { return clients; } + Guard.NotNullOrEmpty(id, nameof(id)); + Guard.NotNull(client, nameof(client)); + + Inner.Add(id, client); } public void Add(string id, string secret) { Guard.NotNullOrEmpty(id, nameof(id)); - clients.Add(id, new AppClient(secret, id)); + Inner.Add(id, new AppClient(id, secret, AppClientPermission.Editor)); } public void Revoke(string id) { Guard.NotNullOrEmpty(id, nameof(id)); - clients.Remove(id); + Inner.Remove(id); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs index 780ed8076..9c4ce924d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs @@ -6,33 +6,25 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Apps { - public class AppContributors + public sealed class AppContributors : DictionaryBase { - private readonly Dictionary contributors = new Dictionary(); - - public IReadOnlyDictionary Contributors - { - get { return contributors; } - } - public void Assign(string contributorId, AppContributorPermission permission) { Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); Guard.Enum(permission, nameof(permission)); - contributors[contributorId] = permission; + Inner[contributorId] = permission; } public void Remove(string contributorId) { Guard.NotNullOrEmpty(contributorId, nameof(contributorId)); - contributors.Remove(contributorId); + Inner.Remove(contributorId); } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs new file mode 100644 index 000000000..908f78c08 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// AppClientsConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public sealed class AppClientsConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var clients = (AppClients)value; + + var json = new Dictionary(clients.Count); + + foreach (var client in clients) + { + json.Add(client.Key, new JsonAppClient(client.Value)); + } + + serializer.Serialize(writer, json); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var json = serializer.Deserialize>(reader); + + var clients = new AppClients(); + + foreach (var client in json) + { + clients.Add(client.Key, client.Value.ToClient()); + } + + return clients; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(AppClients); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs new file mode 100644 index 000000000..98c45ad6e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs @@ -0,0 +1,50 @@ +// ========================================================================== +// AppContributorsConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public sealed class AppContributorsConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var contributors = (AppContributors)value; + + var json = new Dictionary(contributors.Count); + + foreach (var contributor in contributors) + { + json.Add(contributor.Key, contributor.Value); + } + + serializer.Serialize(writer, json); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var json = serializer.Deserialize>(reader); + + var contributors = new AppContributors(); + + foreach (var contributor in json) + { + contributors.Assign(contributor.Key, contributor.Value); + } + + return contributors; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(AppContributors); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs new file mode 100644 index 000000000..c3b11189e --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// JsonAppClient.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public class JsonAppClient + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Secret { get; set; } + + [JsonProperty] + public AppClientPermission Permission { get; set; } + + public JsonAppClient() + { + } + + public JsonAppClient(AppClient client) + { + SimpleMapper.Map(client, this); + } + + public AppClient ToClient() + { + return new AppClient(Name, Secret, Permission); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs new file mode 100644 index 000000000..f85cb70d3 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// JsonLanguageConfig.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public class JsonLanguageConfig + { + [JsonProperty] + public Language[] Fallback { get; set; } + + [JsonProperty] + public bool IsOptional { get; set; } + + public JsonLanguageConfig() + { + } + + public JsonLanguageConfig(LanguageConfig config) + { + SimpleMapper.Map(config, this); + + Fallback = config.LanguageFallbacks.ToArray(); + } + + public LanguageConfig ToConfig(string language) + { + return new LanguageConfig(language, IsOptional, Fallback); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs new file mode 100644 index 000000000..d1f5d6485 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// AppClientsConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public sealed class LanguagesConfigConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var languagesConfig = (LanguagesConfig)value; + + var json = new Dictionary(languagesConfig.Count); + + foreach (var config in languagesConfig.Configs) + { + json.Add(config.Language, new JsonLanguageConfig(config)); + } + + serializer.Serialize(writer, json); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var json = serializer.Deserialize>(reader); + + var languagesConfig = new LanguageConfig[json.Count]; + + var i = 0; + + foreach (var config in json) + { + languagesConfig[i++] = config.Value.ToConfig(config.Key); + } + + return LanguagesConfig.Build(languagesConfig); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(LanguagesConfig); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs similarity index 97% rename from src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs rename to src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index 63fd8904f..b1d507067 100644 --- a/src/Squidex.Domain.Apps.Core.Model/LanguageConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using System.Linq; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Apps { public sealed class LanguageConfig : IFieldPartitionItem { diff --git a/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs similarity index 90% rename from src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs rename to src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index c1d8fb2c8..8f0c6f1f9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -13,7 +13,7 @@ using System.Collections.Immutable; using System.Linq; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Core +namespace Squidex.Domain.Apps.Core.Apps { public sealed class LanguagesConfig : IFieldPartitioning { @@ -24,11 +24,6 @@ namespace Squidex.Domain.Apps.Core get { return state.Master; } } - public int Count - { - get { return state.Languages.Count; } - } - IFieldPartitionItem IFieldPartitioning.Master { get { return state.Master; } @@ -44,6 +39,16 @@ namespace Squidex.Domain.Apps.Core return state.Languages.Values.GetEnumerator(); } + public IEnumerable Configs + { + get { return state.Languages.Values; } + } + + public int Count + { + get { return state.Languages.Count; } + } + private LanguagesConfig(ICollection configs) { Guard.NotNull(configs, nameof(configs)); @@ -83,7 +88,7 @@ namespace Squidex.Domain.Apps.Core { Guard.NotNull(language, nameof(language)); - state = new State( + var newLanguages = state.Languages.Values.Where(x => x.Language != language) .Select(config => { @@ -92,7 +97,14 @@ namespace Squidex.Domain.Apps.Core config.IsOptional, config.LanguageFallbacks.Except(new[] { language })); }) - .ToImmutableDictionary(x => x.Language), state.Master.Language == language ? null : state.Master); + .ToImmutableDictionary(x => x.Language); + + var newMaster = + state.Master.Language != language ? + state.Master : + null; + + state = new State(newLanguages, newMaster); } public bool Contains(Language language) @@ -107,16 +119,18 @@ namespace Squidex.Domain.Apps.Core public bool TryGetItem(string key, out IFieldPartitionItem item) { - item = null; - if (Language.IsValidLanguage(key) && state.Languages.TryGetValue(key, out var value)) { item = value; return true; } + else + { + item = null; - return false; + return false; + } } private sealed class State diff --git a/src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs b/src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs new file mode 100644 index 000000000..6b3734c6a --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/DictionaryBase.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// DictionaryBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections; +using System.Collections.Generic; + +namespace Squidex.Domain.Apps.Core +{ + public abstract class DictionaryBase : IReadOnlyDictionary + { + private readonly Dictionary inner = new Dictionary(); + + public TValue this[TKey key] + { + get { return inner[key]; } + } + + public IEnumerable Keys + { + get { return inner.Keys; } + } + + public IEnumerable Values + { + get { return inner.Values; } + } + + public int Count + { + get { return inner.Count; } + } + + protected Dictionary Inner + { + get { return inner; } + } + + public bool ContainsKey(TKey key) + { + return inner.ContainsKey(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return inner.TryGetValue(key, out value); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return inner.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return inner.GetEnumerator(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs index 334d5b59e..c975e4ef9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using Newtonsoft.Json.Linq; - namespace Squidex.Domain.Apps.Core.Schemas { public abstract class FieldProperties : NamedElementPropertiesBase diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs index dfcbba099..7f5a9d572 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using System; - namespace Squidex.Domain.Apps.Core.Schemas { public abstract class NamedElementPropertiesBase diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs index 30edad325..87c986cb9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ContentConverter.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs index 10f73cb5e..0aabe935c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.Contents; diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs index d7529af47..0cbd275e2 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Domain.Apps.Core.Schemas; diff --git a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs index 7e4442977..54f09a5ef 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Events.Apps.Utils @@ -45,7 +44,7 @@ namespace Squidex.Domain.Apps.Events.Apps.Utils public static void Apply(this AppClients clients, AppClientRenamed @event) { - if (clients.Clients.TryGetValue(@event.Id, out var client)) + if (clients.TryGetValue(@event.Id, out var client)) { client.Rename(@event.Name); } @@ -53,7 +52,7 @@ namespace Squidex.Domain.Apps.Events.Apps.Utils public static void Apply(this AppClients clients, AppClientUpdated @event) { - if (clients.Clients.TryGetValue(@event.Id, out var client)) + if (clients.TryGetValue(@event.Id, out var client)) { client.Update(@event.Permission); } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs index 7810f2814..b7799d63a 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs @@ -39,17 +39,17 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps [BsonRequired] [BsonElement] - [BsonSerializer(typeof(JsonBsonSerializer))] + [BsonJson] public AppClients Clients { get; set; } = new AppClients(); [BsonRequired] [BsonElement] - [BsonSerializer(typeof(JsonBsonSerializer))] + [BsonJson] public AppContributors Contributors { get; set; } = new AppContributors(); [BsonRequired] [BsonElement] - [BsonSerializer(typeof(JsonBsonSerializer))] + [BsonJson] public LanguagesConfig LanguagesConfig { get; } = LanguagesConfig.Build(Language.EN); public PartitionResolver PartitionResolver diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs index aabfa2ebd..c6f475e2e 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs @@ -10,9 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using MongoDB.Bson.Serialization; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Repositories; using Squidex.Infrastructure.CQRS.Events; @@ -22,10 +20,9 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { public partial class MongoAppRepository : MongoRepositoryBase, IAppRepository, IEventConsumer { - public MongoAppRepository(IMongoDatabase database, JsonSerializer serializer) + public MongoAppRepository(IMongoDatabase database) : base(database) { - BsonSerializer.RegisterSerializer(new JsonBsonSerializer(serializer)); } protected override string CollectionName() diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index d4d22106b..d70f8aebf 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -130,7 +130,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps { updater(a); - a.ContributorIds = a.Contributors.Contributors.Keys.ToArray(); + a.ContributorIds = a.Contributors.Keys.ToArray(); }); } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs index bd8e91547..be7dcb916 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/Extensions.cs @@ -10,14 +10,11 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using MongoDB.Bson; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Read.MongoDb.Contents { @@ -25,23 +22,14 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents { private const int MaxLength = 1024 * 1024; - public static BsonDocument ToBsonDocument(this IdContentData data, JsonSerializer jsonSerializer) - { - return (BsonDocument)JToken.FromObject(data, jsonSerializer).ToBson(); - } - public static List ToReferencedIds(this IdContentData data, Schema schema) { return data.GetReferencedIds(schema).ToList(); } - public static NamedContentData ToData(this BsonDocument document, Schema schema, List deletedIds, JsonSerializer jsonSerializer) + public static NamedContentData ToData(this IdContentData idData, Schema schema, List deletedIds) { - return document - .ToJson() - .ToObject(jsonSerializer) - .ToCleanedReferences(schema, new HashSet(deletedIds ?? new List())) - .ToNameModel(schema, true); + return idData.ToCleanedReferences(schema, new HashSet(deletedIds)).ToNameModel(schema, true); } public static string ToFullText(this ContentData data) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs index 5ca934612..141e33e6f 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentEntity.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Newtonsoft.Json; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; @@ -66,10 +65,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents [BsonElement("mb")] public RefToken LastModifiedBy { get; set; } - [BsonRequired] - [BsonElement("do")] - public BsonDocument DataDocument { get; set; } - [BsonRequired] [BsonElement("rf")] public List ReferencedIds { get; set; } @@ -78,14 +73,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents [BsonElement("rd")] public List ReferencedIdsDeleted { get; set; } = new List(); + [BsonRequired] + [BsonElement("do")] + [BsonJson] + public IdContentData IdData { get; set; } + NamedContentData IContentEntity.Data { get { return data; } } - public void ParseData(Schema schema, JsonSerializer serializer) + public void ParseData(Schema schema) { - data = DataDocument.ToData(schema, ReferencedIdsDeleted, serializer); + data = IdData.ToData(schema, ReferencedIdsDeleted); } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs index 4f124278d..5ca896110 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository.cs @@ -13,7 +13,6 @@ using System.Threading.Tasks; using Microsoft.OData.UriParser; using MongoDB.Bson; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Contents; @@ -31,7 +30,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; private readonly ISchemaProvider schemas; - private readonly JsonSerializer serializer; protected static FilterDefinitionBuilder Filter { @@ -65,15 +63,13 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas, JsonSerializer serializer) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas) { Guard.NotNull(database, nameof(database)); Guard.NotNull(schemas, nameof(schemas)); - Guard.NotNull(serializer, nameof(serializer)); this.database = database; this.schemas = schemas; - this.serializer = serializer; } public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) @@ -103,7 +99,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents foreach (var entity in contentEntities) { - entity.ParseData(schema.SchemaDef, serializer); + entity.ParseData(schema.SchemaDef); } return contentEntities; @@ -151,7 +147,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents foreach (var entity in contentEntities) { - entity.ParseData(schema.SchemaDef, serializer); + entity.ParseData(schema.SchemaDef); } return contentEntities.OfType().ToList(); @@ -176,7 +172,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents await collection.Find(x => x.Id == id) .FirstOrDefaultAsync(); - contentEntity?.ParseData(schema.SchemaDef, serializer); + contentEntity?.ParseData(schema.SchemaDef); return contentEntity; } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 61900dd4f..b76240365 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); content.DataText = idData?.ToFullText(); - content.DataDocument = idData?.ToBsonDocument(serializer); + content.IdData = idData; content.ReferencedIds = idData?.ToReferencedIds(schema.SchemaDef); }); }); @@ -90,13 +90,13 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents { return ForSchemaAsync(@event.AppId.Id, @event.SchemaId.Id, (collection, schema) => { - var idData = @event.Data.ToIdModel(schema.SchemaDef, true); + var idData = @event.Data?.ToIdModel(schema.SchemaDef, true); return collection.UpdateOneAsync( Filter.Eq(x => x.Id, @event.ContentId), Update .Set(x => x.DataText, idData.ToFullText()) - .Set(x => x.DataDocument, idData.ToBsonDocument(serializer)) + .Set(x => x.IdData, idData) .Set(x => x.ReferencedIds, idData.ToReferencedIds(schema.SchemaDef)) .Set(x => x.LastModified, headers.Timestamp()) .Set(x => x.LastModifiedBy, @event.Actor) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs index bce8e652d..7efc0059b 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas [BsonRequired] [BsonElement] - [BsonSerializer(typeof(JsonBsonSerializer))] + [BsonJson] public Schema SchemaDef { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs index a4d8ac949..a43da664f 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs @@ -10,9 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using MongoDB.Bson.Serialization; using MongoDB.Driver; -using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Repositories; @@ -26,14 +24,12 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Schemas { private readonly FieldRegistry registry; - public MongoSchemaRepository(IMongoDatabase database, JsonSerializer serializer, FieldRegistry registry) + public MongoSchemaRepository(IMongoDatabase database, FieldRegistry registry) : base(database) { Guard.NotNull(registry, nameof(registry)); this.registry = registry; - - BsonSerializer.RegisterSerializer(new JsonBsonSerializer(serializer)); } protected override string CollectionName() diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs deleted file mode 100644 index 73cae6fba..000000000 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppClientEntity.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// IAppClientEntity.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Squidex.Domain.Apps.Core.Apps; - -namespace Squidex.Domain.Apps.Read.Apps -{ - public interface IAppClientEntity - { - string Name { get; } - - string Secret { get; } - - AppClientPermission Permission { get; } - } -} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs index 327761d38..03f5957bd 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppCommandMiddleware.cs @@ -58,11 +58,6 @@ namespace Squidex.Domain.Apps.Write.Apps }); } - protected Task On(AttachClient command, CommandContext context) - { - return handler.UpdateAsync(context, a => a.AttachClient(command)); - } - protected async Task On(AssignContributor command, CommandContext context) { await handler.UpdateAsync(context, async a => @@ -83,14 +78,34 @@ namespace Squidex.Domain.Apps.Write.Apps }); } + protected Task On(AttachClient command, CommandContext context) + { + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanAttach(a.Clients, command); + + a.AttachClient(command); + }); + } + protected Task On(UpdateClient command, CommandContext context) { - return handler.UpdateAsync(context, a => a.UpdateClient(command)); + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanUpdate(a.Clients, command); + + a.UpdateClient(command); + }); } protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(context, a => a.RevokeClient(command)); + return handler.UpdateAsync(context, a => + { + GuardAppClients.CanRevoke(a.Clients, command); + + a.RevokeClient(command); + }); } protected Task On(AddLanguage command, CommandContext context) diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs index ac265d97e..d891c619a 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppDomainObject.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs index a15d68fb9..4d84ee36c 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using Squidex.Infrastructure; - namespace Squidex.Domain.Apps.Write.Apps.Commands { public sealed class ChangePlan : AppAggregateCommand diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs new file mode 100644 index 000000000..3a19fe5ea --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppClients.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// GuardAppClients.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public static class GuardAppClients + { + public static void CanAttach(AppClients clients, AttachClient command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot attach client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + else if (clients.ContainsKey(command.Id)) + { + error(new ValidationError("Client id already added.", nameof(command.Id))); + } + }); + } + + public static void CanRevoke(AppClients clients, RevokeClient command) + { + Guard.NotNull(command, nameof(command)); + + GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + }); + } + + public static void CanUpdate(AppClients clients, UpdateClient command) + { + Guard.NotNull(command, nameof(command)); + + var client = GetClientOrThrow(clients, command.Id); + + Validate.It(() => "Cannot revoke client.", error => + { + if (string.IsNullOrWhiteSpace(command.Id)) + { + error(new ValidationError("Client id must be defined.", nameof(command.Id))); + } + + if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null) + { + error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission))); + } + + if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue()) + { + error(new ValidationError("Permission is not valid.", nameof(command.Permission))); + } + + if (client != null) + { + if (!string.IsNullOrWhiteSpace(command.Name) && string.Equals(client.Name, command.Name)) + { + error(new ValidationError("Client already has this name.", nameof(command.Permission))); + } + + if (command.Permission == client.Permission) + { + error(new ValidationError("Client already has this permission.", nameof(command.Permission))); + } + } + }); + } + + private static AppClient GetClientOrThrow(AppClients clients, string id) + { + if (id == null) + { + return null; + } + + if (!clients.TryGetValue(id, out var client)) + { + throw new DomainObjectNotFoundException(id, "Clients", typeof(AppDomainObject)); + } + + return client; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs index daee2032b..64ecb94f1 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppContributors.cs @@ -39,14 +39,14 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { error(new ValidationError("Cannot find contributor id.", nameof(command.ContributorId))); } - else if (contributors.Contributors.TryGetValue(command.ContributorId, out var existing)) + else if (contributors.TryGetValue(command.ContributorId, out var existing)) { if (existing == command.Permission) { error(new ValidationError("Contributor has already this permission.", nameof(command.Permission))); } } - else if (plan.MaxContributors == contributors.Contributors.Count) + else if (plan.MaxContributors == contributors.Count) { error(new ValidationError("You have reached the maximum number of contributors for your plan.")); } @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards error(new ValidationError("Contributor id not assigned.", nameof(command.ContributorId))); } - var ownerIds = contributors.Contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); + var ownerIds = contributors.Where(x => x.Value == AppContributorPermission.Owner).Select(x => x.Key).ToList(); if (ownerIds.Count == 1 && ownerIds.Contains(command.ContributorId)) { @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards } }); - if (!contributors.Contributors.ContainsKey(command.ContributorId)) + if (!contributors.ContainsKey(command.ContributorId)) { throw new DomainObjectNotFoundException(command.ContributorId, "Contributors", typeof(AppDomainObject)); } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs index a65e5d05b..1ac39c4b2 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Guards/GuardAppLanguages.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; @@ -39,6 +39,11 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards Validate.It(() => "Cannot remove language.", error => { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + if (languages.Master == languageConfig) { error(new ValidationError("Language config is master.", nameof(command.Language))); @@ -54,6 +59,11 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards Validate.It(() => "Cannot update language.", error => { + if (command.Language == null) + { + error(new ValidationError("Language cannot be null.", nameof(command.Language))); + } + if ((languages.Master == languageConfig || command.IsMaster) && command.IsOptional) { error(new ValidationError("Cannot make master language optional.", nameof(command.IsMaster))); @@ -76,7 +86,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { if (language == null) { - throw new DomainObjectNotFoundException(language, "Languages", typeof(AppDomainObject)); + return null; } if (!languages.TryGetConfig(language, out var languageConfig)) diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs index 410315879..ff38a5638 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/PatchContent.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core.Contents; - namespace Squidex.Domain.Apps.Write.Contents.Commands { public sealed class PatchContent : ContentDataCommand diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs index 47d6128ad..be9546173 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/UpdateContent.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core.Contents; - namespace Squidex.Domain.Apps.Write.Contents.Commands { public sealed class UpdateContent : ContentDataCommand diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 810eb7b8e..d64143b2f 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -7,19 +7,14 @@ // ========================================================================== using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Core.ValidateContent; -using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Assets.Repositories; using Squidex.Domain.Apps.Read.Contents.Repositories; -using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Write.Contents.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; @@ -62,18 +57,18 @@ namespace Squidex.Domain.Apps.Write.Contents { await handler.CreateAsync(context, async content => { - var schemaAndApp = await ResolveSchemaAndAppAsync(command); + GuardContent.CanCreate(command); - ExecuteScriptAndTransform(command, content, schemaAndApp.SchemaEntity.ScriptCreate, "Create"); + var operationContext = await CreateContext(command, content, () => "Failed to create content."); if (command.Publish) { - ExecuteScript(command, content, schemaAndApp.SchemaEntity.ScriptChange, "Published"); + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, "Published"); } - command.Data.Enrich(schemaAndApp.SchemaEntity.SchemaDef, schemaAndApp.AppEntity.PartitionResolver); - - await ValidateAsync(schemaAndApp, command, () => "Failed to create content", false); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptCreate, "Create"); + await operationContext.EnrichAsync(); + await operationContext.ValidateAsync(false); content.Create(command); @@ -85,11 +80,12 @@ namespace Squidex.Domain.Apps.Write.Contents { await handler.UpdateAsync(context, async content => { - var schemaAndApp = await ResolveSchemaAndAppAsync(command); + GuardContent.CanUpdate(command); - ExecuteScriptAndTransform(command, content, schemaAndApp.SchemaEntity.ScriptUpdate, "Update"); + var operationContext = await CreateContext(command, content, () => "Failed to update content."); - await ValidateAsync(schemaAndApp, command, () => "Failed to update content", false); + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Update"); content.Update(command); @@ -101,11 +97,12 @@ namespace Squidex.Domain.Apps.Write.Contents { await handler.UpdateAsync(context, async content => { - var schemaAndApp = await ResolveSchemaAndAppAsync(command); + GuardContent.CanPatch(command); - ExecuteScriptAndTransform(command, content, schemaAndApp.SchemaEntity.ScriptUpdate, "Patch"); + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - await ValidateAsync(schemaAndApp, command, () => "Failed to patch content", true); + await operationContext.ValidateAsync(true); + await operationContext.ExecuteScriptAndTransformAsync(x => x.ScriptUpdate, "Patch"); content.Patch(command); @@ -117,9 +114,11 @@ namespace Squidex.Domain.Apps.Write.Contents { return handler.UpdateAsync(context, async content => { - var schemaAndApp = await ResolveSchemaAndAppAsync(command); + GuardContent.CanChangeContentStatus(content.Status, command); + + var operationContext = await CreateContext(command, content, () => "Failed to patch content."); - ExecuteScript(command, content, schemaAndApp.SchemaEntity.ScriptChange, command.Status); + await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status); content.ChangeStatus(command); }); @@ -129,9 +128,11 @@ namespace Squidex.Domain.Apps.Write.Contents { return handler.UpdateAsync(context, async content => { - var schemaAndApp = await ResolveSchemaAndAppAsync(command); + GuardContent.CanDelete(command); + + var operationContext = await CreateContext(command, content, () => "Failed to delete content."); - ExecuteScript(command, content, schemaAndApp.SchemaEntity.ScriptDelete, "Delete"); + await operationContext.ExecuteScriptAsync(x => x.ScriptDelete, "Delete"); content.Delete(command); }); @@ -145,60 +146,20 @@ namespace Squidex.Domain.Apps.Write.Contents } } - private async Task ValidateAsync((ISchemaEntity Schema, IAppEntity App) schemaAndApp, ContentDataCommand command, Func message, bool partial) + private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message) { - var schemaErrors = new List(); - - var appId = command.AppId.Id; - - var validationContext = - new ValidationContext( - (contentIds, schemaId) => - { - return contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); - }, - assetIds => - { - return assetRepository.QueryNotFoundAsync(appId, assetIds.ToList()); - }); - - if (partial) - { - await command.Data.ValidatePartialAsync(validationContext, schemaAndApp.Schema.SchemaDef, schemaAndApp.App.PartitionResolver, schemaErrors); - } - else - { - await command.Data.ValidateAsync(validationContext, schemaAndApp.Schema.SchemaDef, schemaAndApp.App.PartitionResolver, schemaErrors); - } - - if (schemaErrors.Count > 0) - { - throw new ValidationException(message(), schemaErrors); - } - } - - private void ExecuteScriptAndTransform(ContentDataCommand command, ContentDomainObject content, string script, object operation) - { - var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString(), Data = command.Data }; - - command.Data = scriptEngine.ExecuteAndTransform(ctx, script); - } - - private void ExecuteScript(ContentCommand command, ContentDomainObject content, string script, object operation) - { - var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString() }; - - scriptEngine.Execute(ctx, script); - } - - private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command) - { - var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); - var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); - - await Task.WhenAll(taskForApp, taskForSchema); - - return (taskForSchema.Result, taskForApp.Result); + var operationContext = + await ContentOperationContext.CreateAsync( + contentRepository, + content, + command, + appProvider, + schemas, + scriptEngine, + assetRepository, + message); + + return operationContext; } } } diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs new file mode 100644 index 000000000..72a4ad097 --- /dev/null +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs @@ -0,0 +1,134 @@ +// ========================================================================== +// ContentOperationContext.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.EnrichContent; +using Squidex.Domain.Apps.Core.Scripting; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Read.Apps.Services; +using Squidex.Domain.Apps.Read.Assets.Repositories; +using Squidex.Domain.Apps.Read.Contents.Repositories; +using Squidex.Domain.Apps.Read.Schemas; +using Squidex.Domain.Apps.Read.Schemas.Services; +using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Domain.Apps.Write.Contents +{ + public sealed class ContentOperationContext + { + private ContentDomainObject content; + private ContentCommand command; + private IContentRepository contentRepository; + private IAssetRepository assetRepository; + private IScriptEngine scriptEngine; + private ISchemaEntity schemaEntity; + private IAppEntity appEntity; + private Func message; + + public static async Task CreateAsync( + IContentRepository contentRepository, + ContentDomainObject content, + ContentCommand command, + IAppProvider appProvider, + ISchemaProvider schemas, + IScriptEngine scriptEngine, + IAssetRepository assetRepository, + Func message) + { + var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); + var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); + + await Task.WhenAll(taskForApp, taskForSchema); + + var context = new ContentOperationContext(); + + context.appEntity = taskForApp.Result; + context.assetRepository = assetRepository; + context.contentRepository = contentRepository; + context.content = content; + context.command = command; + context.message = message; + context.schemaEntity = taskForSchema.Result; + context.scriptEngine = scriptEngine; + + return context; + } + + public Task EnrichAsync() + { + if (command is ContentDataCommand dataCommand) + { + dataCommand.Data.Enrich(schemaEntity.SchemaDef, appEntity.PartitionResolver); + } + + return TaskHelper.Done; + } + + public async Task ValidateAsync(bool partial) + { + if (command is ContentDataCommand dataCommand) + { + var errors = new List(); + + var appId = command.AppId.Id; + + var ctx = + new ValidationContext( + (contentIds, schemaId) => + { + return contentRepository.QueryNotFoundAsync(appId, schemaId, contentIds.ToList()); + }, + assetIds => + { + return assetRepository.QueryNotFoundAsync(appId, assetIds.ToList()); + }); + + if (partial) + { + await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver, errors); + } + else + { + await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver, errors); + } + + if (errors.Count > 0) + { + throw new ValidationException(message(), errors.ToArray()); + } + } + } + + public Task ExecuteScriptAndTransformAsync(Func script, object operation) + { + if (command is ContentDataCommand dataCommand) + { + var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString(), Data = dataCommand.Data }; + + dataCommand.Data = scriptEngine.ExecuteAndTransform(ctx, script(schemaEntity)); + } + + return TaskHelper.Done; + } + + public Task ExecuteScriptAsync(Func script, object operation) + { + var ctx = new ScriptContext { ContentId = content.Id, OldData = content.Data, User = command.User, Operation = operation.ToString() }; + + scriptEngine.Execute(ctx, script(schemaEntity)); + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs b/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs index 28b7736ce..06178cdb1 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Guards/GuardContent.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Write.Contents.Guards }); } - public static void CanCreate(UpdateContent command) + public static void CanUpdate(UpdateContent command) { Guard.NotNull(command, nameof(command)); @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Write.Contents.Guards }); } - public static void CanCreate(PatchContent command) + public static void CanPatch(PatchContent command) { Guard.NotNull(command, nameof(command)); @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Write.Contents.Guards }); } - public static void CanChangeStatus(Status status, ChangeContentStatus command) + public static void CanChangeContentStatus(Status status, ChangeContentStatus command) { Guard.NotNull(command, nameof(command)); @@ -65,5 +65,10 @@ namespace Squidex.Domain.Apps.Write.Contents.Guards } }); } + + public static void CanDelete(DeleteContent command) + { + Guard.NotNull(command, nameof(command)); + } } } diff --git a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs index ed0e69533..6502794b5 100644 --- a/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Write/Schemas/Guards/GuardSchema.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System; using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core; diff --git a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs index a18f6c9ee..a2a78ef90 100644 --- a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs +++ b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs @@ -9,7 +9,6 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Squidex.Infrastructure.Security; diff --git a/src/Squidex.Domain.Apps.Read/Apps/IAppContributorEntity.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs similarity index 62% rename from src/Squidex.Domain.Apps.Read/Apps/IAppContributorEntity.cs rename to src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs index 15a6ad8bc..807a46627 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/IAppContributorEntity.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs @@ -1,17 +1,16 @@ // ========================================================================== -// IAppContributorEntity.cs +// BsonJsonAttribute.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Squidex.Domain.Apps.Core.Apps; +using System; -namespace Squidex.Domain.Apps.Read.Apps +namespace Squidex.Infrastructure.MongoDb { - public interface IAppContributorEntity + public sealed class BsonJsonAttribute : Attribute { - AppContributorPermission Permission { get; } } } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs new file mode 100644 index 000000000..e6b485f82 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// BsonJsonConvention.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Reflection; +using MongoDB.Bson.Serialization.Conventions; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.MongoDb +{ + public static class BsonJsonConvention + { + public static void Register(JsonSerializer serializer) + { + var pack = new ConventionPack(); + + var bsonSerializer = new JsonBsonSerializer(serializer); + + pack.AddMemberMapConvention("JsonBson", memberMap => + { + if (memberMap.MemberType.GetCustomAttributes().OfType().Any()) + { + memberMap.SetSerializer(bsonSerializer); + } + }); + + ConventionRegistry.Register("json", pack, t => true); + } + } +} diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index 845f523bb..da9268948 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -14,6 +14,22 @@ namespace Squidex.Infrastructure { public static class CollectionExtensions { + public static bool TryGetValue(this IReadOnlyDictionary values, TKey key, out TBase item) where TValue : TBase + { + if (values.TryGetValue(key, out var value)) + { + item = value; + + return true; + } + else + { + item = default(TBase); + + return false; + } + } + public static int SequentialHashCode(this IEnumerable collection) { return collection.SequentialHashCode(EqualityComparer.Default); diff --git a/src/Squidex.Infrastructure/DictionaryWrapper.cs b/src/Squidex.Infrastructure/DictionaryWrapper.cs deleted file mode 100644 index 609406085..000000000 --- a/src/Squidex.Infrastructure/DictionaryWrapper.cs +++ /dev/null @@ -1,84 +0,0 @@ -// ========================================================================== -// DictionaryWrapper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace Squidex.Infrastructure -{ - public sealed class DictionaryWrapper : IReadOnlyDictionary where TSuper : class, TValue where TValue : class - { - private readonly Func> inner; - - public DictionaryWrapper(Func> inner) - { - Guard.NotNull(inner, nameof(inner)); - - this.inner = inner; - } - - public IEnumerable Keys - { - get { return inner().Keys; } - } - - public IEnumerable Values - { - get { return inner().Values.OfType(); } - } - - public int Count - { - get { return inner().Count; } - } - - public TValue this[TKey key] - { - get { return inner()[key]; } - } - - public bool ContainsKey(TKey key) - { - return inner().ContainsKey(key); - } - - public bool TryGetValue(TKey key, out TValue value) - { - if (inner().TryGetValue(key, out var temp)) - { - value = temp as TValue; - - return value != null; - } - - value = null; - - return false; - } - - public IEnumerator> GetEnumerator() - { - return Enumerate().GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private IEnumerable> Enumerate() - { - foreach (var kvp in inner()) - { - yield return new KeyValuePair(kvp.Key, (TValue)kvp.Value); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 185a1c3a2..9ab3b0040 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -12,12 +12,14 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NodaTime; using NodaTime.Serialization.JsonNet; +using Squidex.Domain.Apps.Core.Apps.Json; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas.Json; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Config.Domain { @@ -32,8 +34,11 @@ namespace Squidex.Config.Domain settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); settings.ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), new InstantConverter(), new LanguageConverter(), + new LanguagesConfigConverter(), new NamedGuidIdConverter(), new NamedLongIdConverter(), new NamedStringIdConverter(), @@ -59,6 +64,8 @@ namespace Squidex.Config.Domain TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); TypeNameRegistry.Map(typeof(NoopEvent).GetTypeInfo().Assembly); + BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings)); + ConfigureJson(SerializerSettings, TypeNameHandling.Auto); } diff --git a/src/Squidex/Config/Identity/LazyClientStore.cs b/src/Squidex/Config/Identity/LazyClientStore.cs index b60f4688e..2fb3a33ca 100644 --- a/src/Squidex/Config/Identity/LazyClientStore.cs +++ b/src/Squidex/Config/Identity/LazyClientStore.cs @@ -13,7 +13,7 @@ using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Read.Apps; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Infrastructure; @@ -64,7 +64,7 @@ namespace Squidex.Config.Identity return client; } - private static Client CreateClientFromApp(string id, IAppClientEntity appClient) + private static Client CreateClientFromApp(string id, AppClient appClient) { return new Client { diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index cc14ca983..b6acd8d12 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -52,7 +52,7 @@ namespace Squidex.Controllers.Api.Apps [ApiCosts(1)] public IActionResult GetContributors(string app) { - var contributors = App.Contributors.Select(x => SimpleMapper.Map(x.Value, new ContributorDto { ContributorId = x.Key })).ToArray(); + var contributors = App.Contributors.Select(x => new ContributorDto { ContributorId = x.Key, Permission = x.Value }).ToArray(); var response = new ContributorsDto { Contributors = contributors, MaxContributors = appPlansProvider.GetPlanForApp(App).MaxContributors }; diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index fcb38bc72..4d0ee698a 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -14,7 +14,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.Api.Apps.Models; -using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; diff --git a/src/Squidex/Controllers/Api/Apps/AppsController.cs b/src/Squidex/Controllers/Api/Apps/AppsController.cs index e1a39b542..dbb119dbd 100644 --- a/src/Squidex/Controllers/Api/Apps/AppsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppsController.cs @@ -61,7 +61,7 @@ namespace Squidex.Controllers.Api.Apps { var dto = SimpleMapper.Map(s, new AppDto()); - dto.Permission = s.Contributors[subject].Permission; + dto.Permission = s.Contributors[subject]; return dto; }).ToList(); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs index a45904268..3bd914770 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/NumberFieldPropertiesDto.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Collections.Immutable; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NJsonSchema.Annotations; @@ -48,11 +47,6 @@ namespace Squidex.Controllers.Api.Schemas.Models { var result = SimpleMapper.Map(this, new NumberFieldProperties()); - if (AllowedValues != null) - { - result.AllowedValues = ImmutableList.Create(AllowedValues); - } - return result; } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs index c36674309..32a3e00da 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/StringFieldPropertiesDto.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Collections.Immutable; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using NJsonSchema.Annotations; @@ -58,11 +57,6 @@ namespace Squidex.Controllers.Api.Schemas.Models { var result = SimpleMapper.Map(this, new StringFieldProperties()); - if (AllowedValues != null) - { - result.AllowedValues = ImmutableList.Create(AllowedValues); - } - return result; } } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index edb868f80..763a7c6fb 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.ContentApi.Models; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Read.Contents; using Squidex.Domain.Apps.Read.Contents.GraphQL; using Squidex.Domain.Apps.Write.Contents; diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs index 094b7c335..82840cb94 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemaSwaggerGenerator.cs @@ -13,8 +13,8 @@ using NJsonSchema; using NSwag; using Squidex.Config; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.JsonSchema; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; using Squidex.Shared.Identity; diff --git a/src/Squidex/Pipeline/AppPermissionAttribute.cs b/src/Squidex/Pipeline/AppPermissionAttribute.cs index 5abfbf000..82517dd38 100644 --- a/src/Squidex/Pipeline/AppPermissionAttribute.cs +++ b/src/Squidex/Pipeline/AppPermissionAttribute.cs @@ -96,9 +96,9 @@ namespace Squidex.Pipeline { var subjectId = user.FindFirst(OpenIdClaims.Subject)?.Value; - if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var contributor)) + if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var permission)) { - return contributor.Permission.ToAppPermission(); + return permission.ToAppPermission(); } return null; diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index d7f56a3ee..a7ffd5aa4 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -23,12 +23,14 @@ - + PreserveNewest + + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs new file mode 100644 index 000000000..99a38afec --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -0,0 +1,103 @@ +// ========================================================================== +// AppClientsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Apps +{ + public class AppClientsTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + private readonly AppClients sut = new AppClients(); + + public AppClientsTests() + { + sut.Add("1", "my-secret"); + } + + [Fact] + public void Should_assign_client() + { + sut.Add("2", "my-secret"); + + sut["2"].ShouldBeEquivalentTo(new AppClient("2", "my-secret", AppClientPermission.Editor)); + } + + [Fact] + public void Should_assign_client_with_permission() + { + sut.Add("2", new AppClient("my-name", "my-secret", AppClientPermission.Reader)); + + sut["2"].ShouldBeEquivalentTo(new AppClient("my-name", "my-secret", AppClientPermission.Reader)); + } + + [Fact] + public void Should_throw_exception_if_assigning_client_with_same_id() + { + sut.Add("2", "my-secret"); + + Assert.Throws(() => sut.Add("2", "my-secret")); + } + + [Fact] + public void Should_rename_client() + { + sut["1"].Rename("my-name"); + + sut["1"].ShouldBeEquivalentTo(new AppClient("my-name", "my-secret", AppClientPermission.Editor)); + } + + [Fact] + public void Should_update_client() + { + sut["1"].Update(AppClientPermission.Reader); + + sut["1"].ShouldBeEquivalentTo(new AppClient("1", "my-secret", AppClientPermission.Reader)); + } + + [Fact] + public void Should_revoke_client() + { + sut.Revoke("1"); + + Assert.Empty(sut); + } + + [Fact] + public void Should_do_nothing_if_client_to_revoke_not_found() + { + sut.Revoke("2"); + + Assert.Single(sut); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + sut.Add("2", "my-secret"); + sut.Add("3", "my-secret"); + sut.Add("4", "my-secret"); + + sut["3"].Update(AppClientPermission.Editor); + + sut["3"].Rename("My Client 3"); + sut["2"].Rename("My Client 2"); + + sut.Revoke("4"); + + var appClients = JToken.FromObject(sut, serializer).ToObject(serializer); + + appClients.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs new file mode 100644 index 000000000..e56b792a9 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppContributorsTests.cs @@ -0,0 +1,70 @@ +// ========================================================================== +// AppContributorsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Apps +{ + public class AppContributorsTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + private readonly AppContributors sut = new AppContributors(); + + [Fact] + public void Should_assign_new_contributor() + { + sut.Assign("1", AppContributorPermission.Developer); + sut.Assign("2", AppContributorPermission.Editor); + + Assert.Equal(AppContributorPermission.Developer, sut["1"]); + Assert.Equal(AppContributorPermission.Editor, sut["2"]); + } + + [Fact] + public void Should_replace_contributor_if_already_exists() + { + sut.Assign("1", AppContributorPermission.Developer); + sut.Assign("1", AppContributorPermission.Owner); + + Assert.Equal(AppContributorPermission.Owner, sut["1"]); + } + + [Fact] + public void Should_remove_contributor() + { + sut.Assign("1", AppContributorPermission.Developer); + sut.Remove("1"); + + Assert.Empty(sut); + } + + [Fact] + public void Should_do_nothing_if_contributor_to_remove_not_found() + { + sut.Remove("2"); + + Assert.Empty(sut); + } + + [Fact] + public void Should_serialize_and_deserialize() + { + sut.Assign("1", AppContributorPermission.Developer); + sut.Assign("2", AppContributorPermission.Editor); + sut.Assign("3", AppContributorPermission.Owner); + + var serialized = JToken.FromObject(sut, serializer).ToObject(serializer); + + serialized.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs new file mode 100644 index 000000000..c32406f9f --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppPlanTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// AppPlanTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Apps +{ + public class AppPlanTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = new AppPlan(new RefToken("user", "Me"), "free"); + + var serialized = JToken.FromObject(sut, serializer).ToObject(serializer); + + serialized.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs new file mode 100644 index 000000000..cd5ae5eb9 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigJsonTests.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// LanguagesConfigJsonTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Apps +{ + public class LanguagesConfigJsonTests + { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); + + [Fact] + public void Should_serialize_and_deserialize() + { + var sut = LanguagesConfig.Build( + new LanguageConfig(Language.EN), + new LanguageConfig(Language.DE, true, Language.EN), + new LanguageConfig(Language.IT, false, Language.DE)); + + var serialized = JToken.FromObject(sut, serializer).ToObject(serializer); + + serialized.ShouldBeEquivalentTo(sut); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs rename to tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index d801a9810..b0830d366 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -11,10 +11,11 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using FluentAssertions; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Domain.Apps.Core.Model +namespace Squidex.Domain.Apps.Core.Model.Apps { public class LanguagesConfigTests { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs index cb4b36c77..f9fe1e6f8 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentDataTests.cs @@ -59,6 +59,23 @@ namespace Squidex.Domain.Apps.Core.Model.Contents Assert.Equal(expected, actual); } + [Fact] + public void Should_return_same_content_if_merging_same_references() + { + var source = + new NamedContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("field2", + new ContentFieldData() + .AddValue("de", 2)); + + var actual = source.MergeInto(source); + + Assert.Same(source, actual); + } + [Fact] public void Should_merge_two_name_models() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs deleted file mode 100644 index 941c85d5e..000000000 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/Json/JsonSerializerTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// JsonSerializerTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Core.Schemas.Json; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json; -using Xunit; - -namespace Squidex.Domain.Apps.Core.Model.Schemas.Json -{ - public class JsonSerializerTests - { - private readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); - private readonly JsonSerializer serializer; - private readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); - - public JsonSerializerTests() - { - serializerSettings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - - serializerSettings.ContractResolver = new ConverterContractResolver( - new InstantConverter(), - new LanguageConverter(), - new NamedGuidIdConverter(), - new NamedLongIdConverter(), - new NamedStringIdConverter(), - new RefTokenConverter(), - new SchemaConverter(new FieldRegistry(typeNameRegistry)), - new StringEnumConverter()); - - serializerSettings.TypeNameHandling = TypeNameHandling.Auto; - - serializer = JsonSerializer.Create(serializerSettings); - } - - [Fact] - public void Should_serialize_and_deserialize_schema() - { - var schemaSource = TestData.MixedSchema(); - var schemaTarget = JToken.FromObject(schemaSource, serializer).ToObject(serializer); - - schemaTarget.ShouldBeEquivalentTo(schemaSource); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs new file mode 100644 index 000000000..02f805529 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// SchemaFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Domain.Apps.Core.Schemas; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Model.Schemas +{ + public class SchemaFieldTests + { + private readonly NumberField sut = new NumberField(1, "my-field", Partitioning.Invariant); + + [Fact] + public void Should_instantiate_field() + { + Assert.Equal("my-field", sut.Name); + } + + [Fact] + public void Should_throw_exception_if_creating_field_with_invalid_name() + { + Assert.Throws(() => new NumberField(1, string.Empty, Partitioning.Invariant)); + } + + [Fact] + public void Should_hide_field() + { + sut.Hide(); + sut.Hide(); + + Assert.True(sut.IsHidden); + } + + [Fact] + public void Should_show_field() + { + sut.Hide(); + sut.Show(); + sut.Show(); + + Assert.False(sut.IsHidden); + } + + [Fact] + public void Should_disable_field() + { + sut.Disable(); + sut.Disable(); + + Assert.True(sut.IsDisabled); + } + + [Fact] + public void Should_enable_field() + { + sut.Disable(); + sut.Enable(); + sut.Enable(); + + Assert.False(sut.IsDisabled); + } + + [Fact] + public void Should_lock_field() + { + sut.Lock(); + + Assert.True(sut.IsLocked); + } + + [Fact] + public void Should_update_field() + { + sut.Update(new NumberFieldProperties { Hints = "my-hints" }); + + Assert.Equal("my-hints", sut.RawProperties.Hints); + } + + [Fact] + public void Should_throw_exception_if_updating_with_invalid_properties_type() + { + Assert.Throws(() => sut.Update(new StringFieldProperties())); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index 8d5fbebfa..93b5006a9 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -9,6 +9,9 @@ using System; using System.Collections.Generic; using System.Linq; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Schemas; using Xunit; @@ -16,6 +19,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas { public class SchemaTests { + private readonly JsonSerializer serializer = TestData.DefaultSerializer(); private readonly Schema sut = new Schema("my-schema"); [Fact] @@ -65,59 +69,11 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas } [Fact] - public void Should_hide_field() - { - AddNumberField(1); - - sut.FieldsById[1].Hide(); - sut.FieldsById[1].Hide(); - - Assert.True(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_show_field() - { - AddNumberField(1); - - sut.FieldsById[1].Hide(); - sut.FieldsById[1].Show(); - sut.FieldsById[1].Show(); - - Assert.False(sut.FieldsById[1].IsHidden); - } - - [Fact] - public void Should_disable_field() - { - AddNumberField(1); - - sut.FieldsById[1].Disable(); - sut.FieldsById[1].Disable(); - - Assert.True(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_enable_field() - { - AddNumberField(1); - - sut.FieldsById[1].Disable(); - sut.FieldsById[1].Enable(); - sut.FieldsById[1].Enable(); - - Assert.False(sut.FieldsById[1].IsDisabled); - } - - [Fact] - public void Should_lock_field() + public void Should_throw_exception_if_updating_with_invalid_properties_type() { AddNumberField(1); - sut.FieldsById[1].Lock(); - - Assert.True(sut.FieldsById[1].IsLocked); + Assert.Throws(() => sut.FieldsById[1].Update(new StringFieldProperties())); } [Fact] @@ -140,24 +96,6 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas Assert.Empty(sut.FieldsById); } - [Fact] - public void Should_update_field() - { - AddNumberField(1); - - sut.FieldsById[1].Update(new NumberFieldProperties { Hints = "my-hints" }); - - Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); - } - - [Fact] - public void Should_throw_exception_if_updating_with_invalid_properties_type() - { - AddNumberField(1); - - Assert.Throws(() => sut.FieldsById[1].Update(new StringFieldProperties())); - } - [Fact] public void Should_publish_schema() { @@ -205,6 +143,15 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); } + [Fact] + public void Should_serialize_and_deserialize_schema() + { + var schemaSource = TestData.MixedSchema(); + var schemaTarget = JToken.FromObject(schemaSource, serializer).ToObject(serializer); + + schemaTarget.ShouldBeEquivalentTo(schemaSource); + } + private NumberField AddNumberField(int id) { var field = new NumberField(id, $"my-field-{id}", Partitioning.Invariant); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs index a6c436d1a..663e00cbd 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ContentConversionTests.cs @@ -6,12 +6,11 @@ // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; -using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Xunit; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs index 5f2b899d0..25b650970 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/EnrichContent/ContentEnrichmentTests.cs @@ -9,6 +9,7 @@ using System; using Newtonsoft.Json.Linq; using NodaTime; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.EnrichContent; using Squidex.Domain.Apps.Core.Schemas; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs index 7f4af1cd6..1c69bdb85 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ExtractReferenceIds; using Squidex.Domain.Apps.Core.Schemas; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs index 9bc7060ab..e76471ee1 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Infrastructure; using Xunit; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index 508e88d38..b786f90a2 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using NJsonSchema; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 7376cd51c..9b0a35e5a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index 33978a938..955fcdc74 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index eda35ca87..c1103da56 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading.Tasks; using FluentAssertions; using Newtonsoft.Json.Linq; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs index 0930c69da..15b4449a0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestData.cs @@ -6,12 +6,45 @@ // All rights reserved. // ========================================================================== +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Squidex.Domain.Apps.Core.Apps.Json; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.Schemas.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core { public static class TestData { + public static JsonSerializer DefaultSerializer() + { + var typeNameRegistry = new TypeNameRegistry(); + + var serializerSettings = new JsonSerializerSettings + { + SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry), + + ContractResolver = new ConverterContractResolver( + new AppClientsConverter(), + new AppContributorsConverter(), + new InstantConverter(), + new LanguageConverter(), + new LanguagesConfigConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new RefTokenConverter(), + new SchemaConverter(new FieldRegistry(typeNameRegistry)), + new StringEnumConverter()), + + TypeNameHandling = TypeNameHandling.Auto + }; + + return JsonSerializer.Create(serializerSettings); + } + public static Schema MixedSchema() { var inv = Partitioning.Invariant; diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs index 2a4419573..a32193a3d 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs @@ -37,28 +37,7 @@ namespace Squidex.Domain.Apps.Read.Contents { private static readonly Guid schemaId = Guid.NewGuid(); private static readonly Guid appId = Guid.NewGuid(); - - private readonly Schema schemaDef = - Schema.Create("my-schema", new SchemaProperties()) - .AddField(new JsonField(1, "my-json", Partitioning.Invariant, - new JsonFieldProperties())) - .AddField(new StringField(2, "my-string", Partitioning.Language, - new StringFieldProperties())) - .AddField(new NumberField(3, "my-number", Partitioning.Invariant, - new NumberFieldProperties())) - .AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, - new DateTimeFieldProperties())) - .AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId })) - .AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })) - .AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, - new GeolocationFieldProperties())); - + private readonly Schema schemaDef = new Schema("my-schema"); private readonly IContentQueryService contentQuery = A.Fake(); private readonly ISchemaRepository schemaRepository = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); @@ -70,6 +49,33 @@ namespace Squidex.Domain.Apps.Read.Contents public GraphQLTests() { + schemaDef.AddField(new JsonField(1, "my-json", Partitioning.Invariant, + new JsonFieldProperties())); + + schemaDef.AddField(new StringField(2, "my-string", Partitioning.Language, + new StringFieldProperties())); + + schemaDef.AddField(new NumberField(3, "my-number", Partitioning.Invariant, + new NumberFieldProperties())); + + schemaDef.AddField(new AssetsField(4, "my-assets", Partitioning.Invariant, + new AssetsFieldProperties())); + + schemaDef.AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant, + new BooleanFieldProperties())); + + schemaDef.AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant, + new DateTimeFieldProperties())); + + schemaDef.AddField(new ReferencesField(7, "my-references", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = schemaId })); + + schemaDef.AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant, + new ReferencesFieldProperties { SchemaId = Guid.NewGuid() })); + + schemaDef.AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant, + new GeolocationFieldProperties())); + A.CallTo(() => app.Id).Returns(appId); A.CallTo(() => app.PartitionResolver).Returns(x => InvariantPartitioning.Instance); diff --git a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs index c34b87fbc..f256b7d47 100644 --- a/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Read.Tests/Contents/ODataQueryTests.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Immutable; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; @@ -15,6 +14,7 @@ using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Contents.Edm; @@ -29,29 +29,11 @@ namespace Squidex.Domain.Apps.Read.Contents { public class ODataQueryTests { - private readonly Schema schemaDef = - Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddField(new StringField(1, "firstName", Partitioning.Language, - new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = new[] { "1", "2" }.ToImmutableList() })) - .AddField(new StringField(2, "lastName", Partitioning.Language, - new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })) - .AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, - new BooleanFieldProperties())) - .AddField(new NumberField(4, "age", Partitioning.Invariant, - new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, - new DateTimeFieldProperties())) - .AddField(new AssetsField(6, "pictures", Partitioning.Invariant, - new AssetsFieldProperties())) - .AddField(new ReferencesField(7, "friends", Partitioning.Invariant, - new ReferencesFieldProperties())) - .AddField(new StringField(8, "dashed-field", Partitioning.Invariant, - new StringFieldProperties())); - + private readonly Schema schemaDef = new Schema("user"); private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); private readonly IEdmModel edmModel; - private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.EN, Language.DE); + private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.EN, Language.DE); static ODataQueryTests() { @@ -60,6 +42,31 @@ namespace Squidex.Domain.Apps.Read.Contents public ODataQueryTests() { + schemaDef.Update(new SchemaProperties { Hints = "The User" }); + + schemaDef.AddField(new StringField(1, "firstName", Partitioning.Language, + new StringFieldProperties { Label = "FirstName", IsRequired = true, AllowedValues = new[] { "1", "2" } })); + schemaDef.AddField(new StringField(2, "lastName", Partitioning.Language, + new StringFieldProperties { Hints = "Last Name", Editor = StringFieldEditor.Input })); + + schemaDef.AddField(new BooleanField(3, "isAdmin", Partitioning.Invariant, + new BooleanFieldProperties())); + + schemaDef.AddField(new NumberField(4, "age", Partitioning.Invariant, + new NumberFieldProperties { MinValue = 1, MaxValue = 10 })); + + schemaDef.AddField(new DateTimeField(5, "birthday", Partitioning.Invariant, + new DateTimeFieldProperties())); + + schemaDef.AddField(new AssetsField(6, "pictures", Partitioning.Invariant, + new AssetsFieldProperties())); + + schemaDef.AddField(new ReferencesField(7, "friends", Partitioning.Invariant, + new ReferencesFieldProperties())); + + schemaDef.AddField(new StringField(8, "dashed-field", Partitioning.Invariant, + new StringFieldProperties())); + var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); var schema = A.Dummy(); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs new file mode 100644 index 000000000..c10fc34d7 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppClientsTests.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// GuardAppClientsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Write.Apps.Commands; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Apps.Guards +{ + public class GuardAppClientsTests + { + private readonly AppClients clients = new AppClients(); + + [Fact] + public void CanAttach_should_throw_execption_if_client_id_is_null() + { + var command = new AttachClient(); + + Assert.Throws(() => GuardAppClients.CanAttach(clients, command)); + } + + [Fact] + public void CanAttach_should_throw_exception_if_client_already_exists() + { + var command = new AttachClient { Id = "android" }; + + clients.Add("android", "secret"); + + Assert.Throws(() => GuardAppClients.CanAttach(clients, command)); + } + + [Fact] + public void CanAttach_should_not_throw_exception_if_client_is_free() + { + var command = new AttachClient { Id = "ios" }; + + clients.Add("android", "secret"); + + GuardAppClients.CanAttach(clients, command); + } + + [Fact] + public void CanRevoke_should_throw_execption_if_client_id_is_null() + { + var command = new RevokeClient(); + + Assert.Throws(() => GuardAppClients.CanRevoke(clients, command)); + } + + [Fact] + public void CanRevoke_should_throw_exception_if_client_is_not_found() + { + var command = new RevokeClient { Id = "ios" }; + + Assert.Throws(() => GuardAppClients.CanRevoke(clients, command)); + } + + [Fact] + public void CanRevoke_should_not_throw_exception_if_client_is_found() + { + var command = new RevokeClient { Id = "ios" }; + + clients.Add("ios", "secret"); + + GuardAppClients.CanRevoke(clients, command); + } + + [Fact] + public void CanUpdate_should_throw_execption_if_client_id_is_null() + { + var command = new UpdateClient(); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_is_not_found() + { + var command = new UpdateClient { Id = "ios", Name = "iOS" }; + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_no_name_and_permission() + { + var command = new UpdateClient { Id = "ios" }; + + clients.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_invalid_permission() + { + var command = new UpdateClient { Id = "ios", Permission = (AppClientPermission)10 }; + + clients.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_same_name() + { + var command = new UpdateClient { Id = "ios", Name = "ios" }; + + clients.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_throw_exception_if_client_has_same_permission() + { + var command = new UpdateClient { Id = "ios", Permission = AppClientPermission.Editor }; + + clients.Add("ios", "secret"); + + Assert.Throws(() => GuardAppClients.CanUpdate(clients, command)); + } + + [Fact] + public void UpdateClient_should_not_throw_exception_if_command_is_valid() + { + var command = new UpdateClient { Id = "ios", Name = "iOS", Permission = AppClientPermission.Reader }; + + clients.Add("ios", "secret"); + + GuardAppClients.CanUpdate(clients, command); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs index f9d007afd..fc59e17a5 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppContributorsTests.cs @@ -21,6 +21,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { private readonly IUserResolver users = A.Fake(); private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly AppContributors contributors = new AppContributors(); public GuardAppContributorsTests() { @@ -36,8 +37,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AssignContributor(); - var contributors = new AppContributors(); - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); } @@ -46,8 +45,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; - var contributors = new AppContributors(); - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); } @@ -56,8 +53,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AssignContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Owner); return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); @@ -71,8 +66,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards var command = new AssignContributor { ContributorId = "1", Permission = (AppContributorPermission)10 }; - var contributors = new AppContributors(); - return Assert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors, command, users, appPlan)); } @@ -84,8 +77,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards var command = new AssignContributor { ContributorId = "3" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Owner); contributors.Assign("2", AppContributorPermission.Editor); @@ -97,8 +88,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AssignContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - return GuardAppContributors.CanAssign(contributors, command, users, appPlan); } @@ -107,8 +96,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AssignContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Editor); return GuardAppContributors.CanAssign(contributors, command, users, appPlan); @@ -122,8 +109,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards var command = new AssignContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Editor); contributors.Assign("2", AppContributorPermission.Editor); @@ -135,8 +120,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveContributor(); - var contributors = new AppContributors(); - Assert.Throws(() => GuardAppContributors.CanRemove(contributors, command)); } @@ -145,8 +128,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - Assert.Throws(() => GuardAppContributors.CanRemove(contributors, command)); } @@ -155,8 +136,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Owner); contributors.Assign("2", AppContributorPermission.Editor); @@ -168,8 +147,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveContributor { ContributorId = "1" }; - var contributors = new AppContributors(); - contributors.Assign("1", AppContributorPermission.Owner); contributors.Assign("2", AppContributorPermission.Owner); diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs index a03087358..02e3ce614 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/Guards/GuardAppLanguagesTests.cs @@ -7,7 +7,7 @@ // ========================================================================== using System.Collections.Generic; -using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure; using Xunit; @@ -16,13 +16,13 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { public class GuardAppLanguagesTests { + private readonly LanguagesConfig languages = LanguagesConfig.Build(Language.DE); + [Fact] public void CanAddLanguage_should_throw_exception_if_language_is_null() { var command = new AddLanguage(); - var languages = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => GuardAppLanguages.CanAdd(languages, command)); } @@ -31,8 +31,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AddLanguage { Language = Language.DE }; - var languages = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => GuardAppLanguages.CanAdd(languages, command)); } @@ -41,8 +39,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new AddLanguage { Language = Language.EN }; - var languages = LanguagesConfig.Build(Language.DE); - GuardAppLanguages.CanAdd(languages, command); } @@ -51,9 +47,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveLanguage(); - var languages = LanguagesConfig.Build(Language.DE); - - Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); + Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); } [Fact] @@ -61,8 +55,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveLanguage { Language = Language.EN }; - var languages = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); } @@ -71,8 +63,6 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveLanguage { Language = Language.DE }; - var languages = LanguagesConfig.Build(Language.DE); - Assert.Throws(() => GuardAppLanguages.CanRemove(languages, command)); } @@ -81,17 +71,27 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new RemoveLanguage { Language = Language.EN }; - var languages = LanguagesConfig.Build(Language.DE, Language.EN); + languages.Set(new LanguageConfig(Language.EN)); GuardAppLanguages.CanRemove(languages, command); } + [Fact] + public void CanUpdateLanguage_should_throw_exception_if_language_is_null() + { + var command = new UpdateLanguage(); + + languages.Set(new LanguageConfig(Language.EN)); + + Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); + } + [Fact] public void CanUpdateLanguage_should_throw_exception_if_language_is_optional_and_master() { var command = new UpdateLanguage { Language = Language.DE, IsOptional = true }; - var languages = LanguagesConfig.Build(Language.DE, Language.EN); + languages.Set(new LanguageConfig(Language.EN)); Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); } @@ -101,7 +101,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.IT } }; - var languages = LanguagesConfig.Build(Language.DE, Language.EN); + languages.Set(new LanguageConfig(Language.EN)); Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); } @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new UpdateLanguage { Language = Language.IT }; - var languages = LanguagesConfig.Build(Language.DE, Language.EN); + languages.Set(new LanguageConfig(Language.EN)); Assert.Throws(() => GuardAppLanguages.CanUpdate(languages, command)); } @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Write.Apps.Guards { var command = new UpdateLanguage { Language = Language.DE, Fallback = new List { Language.EN } }; - var languages = LanguagesConfig.Build(Language.DE, Language.EN); + languages.Set(new LanguageConfig(Language.EN)); GuardAppLanguages.CanUpdate(languages, command); } diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs index 9d02d19b5..3ea088770 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -11,6 +11,7 @@ using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; 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; diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs new file mode 100644 index 000000000..b0f78570d --- /dev/null +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/Guard/GuardContentTests.cs @@ -0,0 +1,99 @@ +// ========================================================================== +// GuardContentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Write.Contents.Commands; +using Squidex.Domain.Apps.Write.Contents.Guards; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Write.Contents.Guard +{ + public class GuardContentTests + { + [Fact] + public void CanCreate_should_throw_exception_if_data_is_null() + { + var command = new CreateContent(); + + Assert.Throws(() => GuardContent.CanCreate(command)); + } + + [Fact] + public void CanCreate_should_not_throw_exception_if_data_is_not_null() + { + var command = new CreateContent { Data = new NamedContentData() }; + + GuardContent.CanCreate(command); + } + + [Fact] + public void CanUpdate_should_throw_exception_if_data_is_null() + { + var command = new UpdateContent(); + + Assert.Throws(() => GuardContent.CanUpdate(command)); + } + + [Fact] + public void CanUpdate_should_not_throw_exception_if_data_is_not_null() + { + var command = new UpdateContent { Data = new NamedContentData() }; + + GuardContent.CanUpdate(command); + } + + [Fact] + public void CanPatch_should_throw_exception_if_data_is_null() + { + var command = new PatchContent(); + + Assert.Throws(() => GuardContent.CanPatch(command)); + } + + [Fact] + public void CanPatch_should_not_throw_exception_if_data_is_not_null() + { + var command = new PatchContent { Data = new NamedContentData() }; + + GuardContent.CanPatch(command); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_not_valid() + { + var command = new ChangeContentStatus { Status = (Status)10 }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_should_throw_exception_if_status_flow_not_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + Assert.Throws(() => GuardContent.CanChangeContentStatus(Status.Archived, command)); + } + + [Fact] + public void CanChangeContentStatus_not_should_throw_exception_if_status_flow_valid() + { + var command = new ChangeContentStatus { Status = Status.Published }; + + GuardContent.CanChangeContentStatus(Status.Draft, command); + } + + [Fact] + public void CanPatch_should_not_throw_exception() + { + var command = new DeleteContent(); + + GuardContent.CanDelete(command); + } + } +} From 655270d1a03481557194c934d799c209fe9a27bc Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 18:08:06 +0200 Subject: [PATCH 06/10] Serializer fixed. --- .../Apps/Utils/AppEventDispatcher.cs | 14 ++++++++++++-- .../MongoDb/BsonJsonAttribute.cs | 1 + .../MongoDb/BsonJsonConvention.cs | 13 +++++++++---- .../MongoDb/JsonBsonSerializer.cs | 8 ++++---- .../CQRS/Events/CompoundEventConsumer.cs | 6 +++++- src/Squidex/Config/Domain/Serializers.cs | 4 ++-- src/Squidex/Pipeline/ActionContextLogAppender.cs | 5 +++++ 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs index 54f09a5ef..e6b4a9998 100644 --- a/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs +++ b/src/Squidex.Domain.Apps.Events/Apps/Utils/AppEventDispatcher.cs @@ -1,11 +1,12 @@ // ========================================================================== -// MongoAppRepository_EventHandling.cs +// AppEventDispatcher.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== +using System.Linq; using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Events.Apps.Utils @@ -60,7 +61,16 @@ namespace Squidex.Domain.Apps.Events.Apps.Utils public static void Apply(this LanguagesConfig languagesConfig, AppLanguageUpdated @event) { - languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, @event.Fallback)); + var fallback = @event.Fallback; + + if (fallback != null && fallback.Count > 0) + { + var existingLangauges = languagesConfig.OfType().Select(x => x.Language); + + fallback = fallback.Intersect(existingLangauges).ToList(); + } + + languagesConfig.Set(new LanguageConfig(@event.Language, @event.IsOptional, fallback)); if (@event.IsMaster) { diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs index 807a46627..738835e47 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonAttribute.cs @@ -10,6 +10,7 @@ using System; namespace Squidex.Infrastructure.MongoDb { + [AttributeUsage(AttributeTargets.Property)] public sealed class BsonJsonAttribute : Attribute { } diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index e6b485f82..62f163f47 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -6,8 +6,10 @@ // All rights reserved. // ========================================================================== +using System; using System.Linq; using System.Reflection; +using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using Newtonsoft.Json; @@ -19,13 +21,16 @@ namespace Squidex.Infrastructure.MongoDb { var pack = new ConventionPack(); - var bsonSerializer = new JsonBsonSerializer(serializer); - pack.AddMemberMapConvention("JsonBson", memberMap => { - if (memberMap.MemberType.GetCustomAttributes().OfType().Any()) + var attributes = memberMap.MemberInfo.GetCustomAttributes(); + + if (attributes.OfType().Any()) { - memberMap.SetSerializer(bsonSerializer); + var bsonSerializerType = typeof(JsonBsonSerializer<>).MakeGenericType(memberMap.MemberType); + var bsonSerializer = Activator.CreateInstance(bsonSerializerType, serializer); + + memberMap.SetSerializer((IBsonSerializer)bsonSerializer); } }); diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs index a2385e726..996317c7b 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/JsonBsonSerializer.cs @@ -14,7 +14,7 @@ using Newtonsoft.Json.Linq; namespace Squidex.Infrastructure.MongoDb { - public class JsonBsonSerializer : ClassSerializerBase + public class JsonBsonSerializer : ClassSerializerBase where T : class { private readonly JsonSerializer serializer; @@ -25,12 +25,12 @@ namespace Squidex.Infrastructure.MongoDb this.serializer = serializer; } - protected override object DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) + protected override T DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args) { - return BsonSerializer.Deserialize(context.Reader).ToJson().ToObject(args.NominalType, serializer); + return BsonSerializer.Deserialize(context.Reader).ToJson().ToObject(serializer); } - protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, object value) + protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, T value) { BsonSerializer.Serialize(context.Writer, JObject.FromObject(value, serializer).ToBson()); } diff --git a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs b/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs index 93be936c1..ae167f3a5 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs @@ -34,7 +34,11 @@ namespace Squidex.Infrastructure.CQRS.Events Name = name; - EventsFilter = string.Join("|", this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)).Select(x => $"({x.EventsFilter})")); + var innerFilters = + this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter)) + .Select(x => $"({x.EventsFilter})"); + + EventsFilter = string.Join("|", innerFilters); } public Task ClearAsync() diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 9ab3b0040..dd1495265 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -64,9 +64,9 @@ namespace Squidex.Config.Domain TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); TypeNameRegistry.Map(typeof(NoopEvent).GetTypeInfo().Assembly); - BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings)); - ConfigureJson(SerializerSettings, TypeNameHandling.Auto); + + BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings)); } public static IServiceCollection AddMyEventFormatter(this IServiceCollection services) diff --git a/src/Squidex/Pipeline/ActionContextLogAppender.cs b/src/Squidex/Pipeline/ActionContextLogAppender.cs index bfe97e06b..a56837143 100644 --- a/src/Squidex/Pipeline/ActionContextLogAppender.cs +++ b/src/Squidex/Pipeline/ActionContextLogAppender.cs @@ -32,6 +32,11 @@ namespace Squidex.Pipeline var httpContext = actionContext.HttpContext; + if (string.IsNullOrEmpty(httpContext.Request.Method)) + { + return; + } + Guid requestId; if (httpContext.Items.TryGetValue(nameof(requestId), out var value) && value is Guid requestIdValue) From 5d30aecf408034f8607ad2498aaf368c435508bb Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 18:34:11 +0200 Subject: [PATCH 07/10] Serialization of Languages config fixed. --- .../Apps/LanguagesConfig.cs | 9 +++------ .../Apps/MongoAppEntity.cs | 6 +++--- .../Apps/MongoAppRepository_EventHandling.cs | 7 +++++++ .../settings/pages/clients/clients-page.component.ts | 2 +- .../Model/Apps/LanguagesConfigTests.cs | 8 -------- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 8f0c6f1f9..55d9101f7 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -13,6 +13,8 @@ using System.Collections.Immutable; using System.Linq; using Squidex.Infrastructure; +#pragma warning disable IDE0016 // Use 'throw' expression + namespace Squidex.Domain.Apps.Core.Apps { public sealed class LanguagesConfig : IFieldPartitioning @@ -102,7 +104,7 @@ namespace Squidex.Domain.Apps.Core.Apps var newMaster = state.Master.Language != language ? state.Master : - null; + state.Languages.Values.FirstOrDefault(); state = new State(newLanguages, newMaster); } @@ -161,11 +163,6 @@ namespace Squidex.Domain.Apps.Core.Apps throw new InvalidOperationException("Config has no master language."); } - if (master.IsOptional) - { - throw new InvalidOperationException("Config has an optional master language."); - } - this.Master = master; } } diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs index b7799d63a..b86fd0f75 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs @@ -40,17 +40,17 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps [BsonRequired] [BsonElement] [BsonJson] - public AppClients Clients { get; set; } = new AppClients(); + public AppClients Clients { get; set; } [BsonRequired] [BsonElement] [BsonJson] - public AppContributors Contributors { get; set; } = new AppContributors(); + public AppContributors Contributors { get; set; } [BsonRequired] [BsonElement] [BsonJson] - public LanguagesConfig LanguagesConfig { get; } = LanguagesConfig.Build(Language.EN); + public LanguagesConfig LanguagesConfig { get; set; } public PartitionResolver PartitionResolver { diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index d70f8aebf..0fdab29f8 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -9,10 +9,12 @@ using System; using System.Linq; using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps.Utils; using Squidex.Domain.Apps.Read.MongoDb.Utils; +using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; @@ -41,6 +43,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Apps return Collection.CreateAsync(@event, headers, a => { SimpleMapper.Map(@event, a); + + a.Clients = new AppClients(); + a.Contributors = new AppContributors(); + + a.LanguagesConfig = LanguagesConfig.Build(Language.EN); }); } diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts index 1c89ec2a1..ed7d13015 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts @@ -113,9 +113,9 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { .switchMap(app => this.appClientsService.postClient(app, requestDto, this.appClients.version)) .subscribe(dto => { this.updateClients(this.appClients.addClient(dto.payload, dto.version)); + this.resetClientForm(); }, error => { this.notifyError(error); - }, () => { this.resetClientForm(); }); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index b0830d366..1614dc8ef 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -204,14 +204,6 @@ namespace Squidex.Domain.Apps.Core.Model.Apps Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, false, Language.EN))); } - [Fact] - public void Should_throw_exception_if_language_to_make_optional_is_master_language() - { - var config = LanguagesConfig.Build(Language.DE); - - Assert.Throws(() => config.Set(new LanguageConfig(Language.DE, true))); - } - [Fact] public void Should_provide_enumerators() { From 510109fd0567ad9e7c75b2c80b2943c09a8ce426 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 18:40:21 +0200 Subject: [PATCH 08/10] Bugfixes --- .../Apps/MongoAppEntity.cs | 1 - .../CQRS/Events/GetEventStoreSubscription.cs | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs index b86fd0f75..46e1f5e9e 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppEntity.cs @@ -10,7 +10,6 @@ using MongoDB.Bson.Serialization.Attributes; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Read.Apps; -using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Read.MongoDb.Apps diff --git a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs index 8f6ae46d3..53799cf95 100644 --- a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs @@ -20,7 +20,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Events { - internal sealed class GetEventStoreSubscription : DisposableObjectBase, IEventSubscription + internal sealed class GetEventStoreSubscription : IEventSubscription { private const string ProjectionName = "by-{0}-{1}"; private static readonly ConcurrentDictionary SubscriptionsCreated = new ConcurrentDictionary(); @@ -57,12 +57,11 @@ namespace Squidex.Infrastructure.CQRS.Events subscription = SubscribeToStream(streamName); } - protected override void DisposeObject(bool disposing) + public Task StopAsync() { - if (disposing) - { - subscription.Stop(); - } + subscription.Stop(); + + return TaskHelper.Done; } private EventStoreCatchUpSubscription SubscribeToStream(string streamName) From 17905c000e7a13ccfcd758a9ed5a02d950e87347 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 19:47:58 +0200 Subject: [PATCH 09/10] Event number fixed --- .../CQRS/Events/MongoEvent.cs | 15 +++++++++++++++ .../CQRS/Events/MongoEventStore.cs | 4 ++-- .../CQRS/Events/Actors/EventConsumerActor.cs | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEvent.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEvent.cs index b7fd565eb..09d12b024 100644 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEvent.cs +++ b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEvent.cs @@ -8,6 +8,7 @@ using System; using MongoDB.Bson.Serialization.Attributes; +using Squidex.Infrastructure.Reflection; namespace Squidex.Infrastructure.CQRS.Events { @@ -28,5 +29,19 @@ namespace Squidex.Infrastructure.CQRS.Events [BsonElement] [BsonRequired] public string Type { get; set; } + + public MongoEvent() + { + } + + public MongoEvent(EventData data) + { + SimpleMapper.Map(data, this); + } + + public EventData ToEventData() + { + return SimpleMapper.Map(this, new EventData()); + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs index 54098fe0d..2abb9c2cb 100644 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs @@ -99,7 +99,7 @@ namespace Squidex.Infrastructure.CQRS.Events if (commitOffset > lastPosition.CommitOffset || commitTimestamp > lastPosition.Timestamp) { - var eventData = new EventData { EventId = e.EventId, Metadata = e.Metadata, Payload = e.Payload, Type = e.Type }; + var eventData = e.ToEventData(); var eventToken = new StreamPosition(commitTimestamp, commitOffset, commit.Events.Length); await callback(new StoredEvent(eventToken, eventStreamOffset, eventData)); @@ -232,7 +232,7 @@ namespace Squidex.Infrastructure.CQRS.Events foreach (var e in events) { - var mongoEvent = new MongoEvent { EventId = e.EventId, Metadata = e.Metadata, Payload = e.Payload, Type = e.Type }; + var mongoEvent = new MongoEvent(e); commitEvents[i++] = mongoEvent; } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs index 90ab63426..1fbb026ae 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs @@ -99,7 +99,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Actors return DoAndUpdateStateAsync(async () => { - await DispatchConsumerAsync(formatter.Parse(storedEvent.Data)); + await DispatchConsumerAsync(ParseEvent(storedEvent)); statusError = null; statusPosition = storedEvent.EventPosition; From 38586e4b1a1696c58ab1e5b5d99f7826ec3f3fa8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 24 Oct 2017 19:43:18 +0200 Subject: [PATCH 10/10] Tests fixed --- .../Apps/Json/AppClientsConverter.cs | 19 ++---- .../Apps/Json/AppContributorsConverter.cs | 19 ++---- .../Apps/Json/LanguagesConfigConverter.cs | 19 ++---- .../Apps/LanguagesConfig.cs | 5 -- .../Schemas/Json/SchemaConverter.cs | 15 ++--- .../Json/ClaimsPrincipalConverter.cs | 62 +++++++++++++++++++ .../Json/JsonClassConverter.cs | 46 ++++++++++++++ .../Json/LanguageConverter.cs | 19 +++--- .../Json/NamedGuidIdConverter.cs | 21 +++---- .../Json/NamedLongIdConverter.cs | 21 +++---- .../Json/NamedStringIdConverter.cs | 21 +++---- .../Json/PropertiesBagConverter.cs | 51 ++++++++------- .../Json/RefTokenConverter.cs | 17 +++-- src/Squidex/Config/Domain/Serializers.cs | 1 + .../Model/Apps/LanguagesConfigTests.cs | 8 --- .../Json/ClaimsPrincipalConverterTests.cs | 54 ++++++++++++++++ .../Json/InstantConverterTests.cs | 16 +++++ .../LanguageTests.cs | 28 +++++---- .../NamedIdTests.cs | 24 +++++-- .../RefTokenTests.cs | 8 ++- .../TestHelpers/JsonHelper.cs | 9 ++- 21 files changed, 315 insertions(+), 168 deletions(-) create mode 100644 src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs create mode 100644 src/Squidex.Infrastructure/Json/JsonClassConverter.cs create mode 100644 tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs index 908f78c08..16570c87b 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -6,21 +6,19 @@ // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; using Newtonsoft.Json; +using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.Apps.Json { - public sealed class AppClientsConverter : JsonConverter + public sealed class AppClientsConverter : JsonClassConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, AppClients value, JsonSerializer serializer) { - var clients = (AppClients)value; + var json = new Dictionary(value.Count); - var json = new Dictionary(clients.Count); - - foreach (var client in clients) + foreach (var client in value) { json.Add(client.Key, new JsonAppClient(client.Value)); } @@ -28,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override AppClients ReadValue(JsonReader reader, JsonSerializer serializer) { var json = serializer.Deserialize>(reader); @@ -41,10 +39,5 @@ namespace Squidex.Domain.Apps.Core.Apps.Json return clients; } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(AppClients); - } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs index 98c45ad6e..efa326377 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs @@ -6,21 +6,19 @@ // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; using Newtonsoft.Json; +using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.Apps.Json { - public sealed class AppContributorsConverter : JsonConverter + public sealed class AppContributorsConverter : JsonClassConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, AppContributors value, JsonSerializer serializer) { - var contributors = (AppContributors)value; + var json = new Dictionary(value.Count); - var json = new Dictionary(contributors.Count); - - foreach (var contributor in contributors) + foreach (var contributor in value) { json.Add(contributor.Key, contributor.Value); } @@ -28,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override AppContributors ReadValue(JsonReader reader, JsonSerializer serializer) { var json = serializer.Deserialize>(reader); @@ -41,10 +39,5 @@ namespace Squidex.Domain.Apps.Core.Apps.Json return contributors; } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(AppContributors); - } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs index d1f5d6485..af52f34a9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs @@ -6,21 +6,19 @@ // All rights reserved. // ========================================================================== -using System; using System.Collections.Generic; using Newtonsoft.Json; +using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.Apps.Json { - public sealed class LanguagesConfigConverter : JsonConverter + public sealed class LanguagesConfigConverter : JsonClassConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, LanguagesConfig value, JsonSerializer serializer) { - var languagesConfig = (LanguagesConfig)value; + var json = new Dictionary(value.Count); - var json = new Dictionary(languagesConfig.Count); - - foreach (var config in languagesConfig.Configs) + foreach (LanguageConfig config in value) { json.Add(config.Language, new JsonLanguageConfig(config)); } @@ -28,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Apps.Json serializer.Serialize(writer, json); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override LanguagesConfig ReadValue(JsonReader reader, JsonSerializer serializer) { var json = serializer.Deserialize>(reader); @@ -43,10 +41,5 @@ namespace Squidex.Domain.Apps.Core.Apps.Json return LanguagesConfig.Build(languagesConfig); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(LanguagesConfig); - } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs index 55d9101f7..57f857877 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs @@ -41,11 +41,6 @@ namespace Squidex.Domain.Apps.Core.Apps return state.Languages.Values.GetEnumerator(); } - public IEnumerable Configs - { - get { return state.Languages.Values; } - } - public int Count { get { return state.Languages.Count; } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs index 5502fd3f9..317afa717 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/SchemaConverter.cs @@ -6,13 +6,13 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json; using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.Schemas.Json { - public sealed class SchemaConverter : JsonConverter + public sealed class SchemaConverter : JsonClassConverter { private readonly FieldRegistry fieldRegistry; @@ -23,19 +23,14 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json this.fieldRegistry = fieldRegistry; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, Schema value, JsonSerializer serializer) { - serializer.Serialize(writer, new JsonSchemaModel((Schema)value)); + serializer.Serialize(writer, new JsonSchemaModel(value)); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override Schema ReadValue(JsonReader reader, JsonSerializer serializer) { return serializer.Deserialize(reader).ToSchema(fieldRegistry); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Schema); - } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs b/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs new file mode 100644 index 000000000..aca3cdf7f --- /dev/null +++ b/src/Squidex.Infrastructure/Json/ClaimsPrincipalConverter.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// ClaimsPrincipalConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json +{ + public sealed class ClaimsPrincipalConverter : JsonClassConverter + { + private sealed class JsonIdentity + { + [JsonProperty] + public string AuthenticationType { get; set; } + + [JsonProperty] + public JsonClaim[] Claims { get; set; } + } + + private sealed class JsonClaim + { + [JsonProperty] + public string Type { get; set; } + + [JsonProperty] + public string Value { get; set; } + } + + protected override void WriteValue(JsonWriter writer, ClaimsPrincipal value, JsonSerializer serializer) + { + var jsonIdentities = + value.Identities.Select(identity => + new JsonIdentity + { + Claims = identity.Claims.Select(c => + { + return new JsonClaim { Type = c.Type, Value = c.Value }; + }).ToArray(), + AuthenticationType = identity.AuthenticationType + }).ToArray(); + + serializer.Serialize(writer, jsonIdentities); + } + + protected override ClaimsPrincipal ReadValue(JsonReader reader, JsonSerializer serializer) + { + var jsonIdentities = serializer.Deserialize(reader); + + return new ClaimsPrincipal( + jsonIdentities.Select(identity => + new ClaimsIdentity( + identity.Claims.Select(c => new Claim(c.Type, c.Value)), + identity.AuthenticationType))); + } + } +} diff --git a/src/Squidex.Infrastructure/Json/JsonClassConverter.cs b/src/Squidex.Infrastructure/Json/JsonClassConverter.cs new file mode 100644 index 000000000..a784d6397 --- /dev/null +++ b/src/Squidex.Infrastructure/Json/JsonClassConverter.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// JsonClassConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json +{ + public abstract class JsonClassConverter : JsonConverter where T : class + { + public sealed override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + return ReadValue(reader, serializer); + } + + public sealed override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + WriteValue(writer, (T)value, serializer); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + + protected abstract void WriteValue(JsonWriter writer, T value, JsonSerializer serializer); + + protected abstract T ReadValue(JsonReader reader, JsonSerializer serializer); + } +} diff --git a/src/Squidex.Infrastructure/Json/LanguageConverter.cs b/src/Squidex.Infrastructure/Json/LanguageConverter.cs index 38cacbba3..25d2fa296 100644 --- a/src/Squidex.Infrastructure/Json/LanguageConverter.cs +++ b/src/Squidex.Infrastructure/Json/LanguageConverter.cs @@ -6,26 +6,25 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json { - public sealed class LanguageConverter : JsonConverter + public sealed class LanguageConverter : JsonClassConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, Language value, JsonSerializer serializer) { - writer.WriteValue(((Language)value).Iso2Code); + writer.WriteValue(value.Iso2Code); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override Language ReadValue(JsonReader reader, JsonSerializer serializer) { - return reader.TokenType == JsonToken.Null ? null : Language.GetLanguage((string)reader.Value); - } + if (reader.TokenType != JsonToken.String) + { + throw new JsonException($"Expected String, but got {reader.TokenType}."); + } - public override bool CanConvert(Type objectType) - { - return objectType == typeof(Language); + return Language.GetLanguage(reader.Value.ToString()); } } } diff --git a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs index a594d773e..9e572d2c3 100644 --- a/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedGuidIdConverter.cs @@ -12,23 +12,21 @@ using Newtonsoft.Json; namespace Squidex.Infrastructure.Json { - public sealed class NamedGuidIdConverter : JsonConverter + public sealed class NamedGuidIdConverter : JsonClassConverter> { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, NamedId value, JsonSerializer serializer) { - var namedId = (NamedId)value; - - writer.WriteValue($"{namedId.Id},{namedId.Name}"); + writer.WriteValue($"{value.Id},{value.Name}"); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) + if (reader.TokenType != JsonToken.String) { - return null; + throw new JsonException($"Expected String, but got {reader.TokenType}."); } - var parts = ((string)reader.Value).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = reader.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) { @@ -42,10 +40,5 @@ namespace Squidex.Infrastructure.Json return new NamedId(id, string.Join(",", parts.Skip(1))); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(NamedId); - } } } diff --git a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs index 1993fd1f2..f81b62650 100644 --- a/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedLongIdConverter.cs @@ -12,23 +12,21 @@ using Newtonsoft.Json; namespace Squidex.Infrastructure.Json { - public sealed class NamedLongIdConverter : JsonConverter + public sealed class NamedLongIdConverter : JsonClassConverter> { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, NamedId value, JsonSerializer serializer) { - var namedId = (NamedId)value; - - writer.WriteValue($"{namedId.Id},{namedId.Name}"); + writer.WriteValue($"{value.Id},{value.Name}"); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) + if (reader.TokenType != JsonToken.String) { - return null; + throw new JsonException($"Expected String, but got {reader.TokenType}."); } - var parts = ((string)reader.Value).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = reader.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) { @@ -42,10 +40,5 @@ namespace Squidex.Infrastructure.Json return new NamedId(id, string.Join(",", parts.Skip(1))); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(NamedId); - } } } diff --git a/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs b/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs index ddc81b39c..4fb5a379e 100644 --- a/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs +++ b/src/Squidex.Infrastructure/Json/NamedStringIdConverter.cs @@ -12,23 +12,21 @@ using Newtonsoft.Json; namespace Squidex.Infrastructure.Json { - public sealed class NamedStringIdConverter : JsonConverter + public sealed class NamedStringIdConverter : JsonClassConverter> { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, NamedId value, JsonSerializer serializer) { - var namedId = (NamedId)value; - - writer.WriteValue($"{namedId.Id},{namedId.Name}"); + writer.WriteValue($"{value.Id},{value.Name}"); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override NamedId ReadValue(JsonReader reader, JsonSerializer serializer) { - if (reader.TokenType == JsonToken.Null) + if (reader.TokenType != JsonToken.String) { - return null; + throw new JsonException($"Expected String, but got {reader.TokenType}."); } - var parts = ((string)reader.Value).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var parts = reader.Value.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) { @@ -37,10 +35,5 @@ namespace Squidex.Infrastructure.Json return new NamedId(parts[0], string.Join(",", parts.Skip(1))); } - - public override bool CanConvert(Type objectType) - { - return objectType == typeof(NamedId); - } } } diff --git a/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs b/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs index f75bf6578..fad3d3243 100644 --- a/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs +++ b/src/Squidex.Infrastructure/Json/PropertiesBagConverter.cs @@ -13,15 +13,36 @@ using NodaTime.Extensions; namespace Squidex.Infrastructure.Json { - public sealed class PropertiesBagConverter : JsonConverter + public sealed class PropertiesBagConverter : JsonClassConverter { - public override bool CanConvert(Type objectType) + protected override void WriteValue(JsonWriter writer, PropertiesBag value, JsonSerializer serializer) { - return typeof(PropertiesBag).IsAssignableFrom(objectType); + writer.WriteStartObject(); + + foreach (var kvp in value.Properties) + { + writer.WritePropertyName(kvp.Key); + + if (kvp.Value.RawValue is Instant) + { + writer.WriteValue(kvp.Value.ToString()); + } + else + { + writer.WriteValue(kvp.Value.RawValue); + } + } + + writer.WriteEndObject(); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override PropertiesBag ReadValue(JsonReader reader, JsonSerializer serializer) { + if (reader.TokenType != JsonToken.StartObject) + { + throw new JsonException($"Expected Object, but got {reader.TokenType}."); + } + var properties = new PropertiesBag(); while (reader.Read()) @@ -50,27 +71,9 @@ namespace Squidex.Infrastructure.Json return properties; } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override bool CanConvert(Type objectType) { - var properties = (PropertiesBag)value; - - writer.WriteStartObject(); - - foreach (var kvp in properties.Properties) - { - writer.WritePropertyName(kvp.Key); - - if (kvp.Value.RawValue is Instant) - { - writer.WriteValue(kvp.Value.ToString()); - } - else - { - writer.WriteValue(kvp.Value.RawValue); - } - } - - writer.WriteEndObject(); + return typeof(PropertiesBag).IsAssignableFrom(objectType); } } } diff --git a/src/Squidex.Infrastructure/Json/RefTokenConverter.cs b/src/Squidex.Infrastructure/Json/RefTokenConverter.cs index 7dcb1d3db..9964c7f3a 100644 --- a/src/Squidex.Infrastructure/Json/RefTokenConverter.cs +++ b/src/Squidex.Infrastructure/Json/RefTokenConverter.cs @@ -6,26 +6,25 @@ // All rights reserved. // ========================================================================== -using System; using Newtonsoft.Json; namespace Squidex.Infrastructure.Json { - public sealed class RefTokenConverter : JsonConverter + public sealed class RefTokenConverter : JsonClassConverter { - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + protected override void WriteValue(JsonWriter writer, RefToken value, JsonSerializer serializer) { writer.WriteValue(value.ToString()); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + protected override RefToken ReadValue(JsonReader reader, JsonSerializer serializer) { - return reader.TokenType == JsonToken.Null ? null : RefToken.Parse((string)reader.Value); - } + if (reader.TokenType != JsonToken.String) + { + throw new JsonException($"Expected String, but got {reader.TokenType}."); + } - public override bool CanConvert(Type objectType) - { - return objectType == typeof(RefToken); + return RefToken.Parse(reader.Value.ToString()); } } } \ No newline at end of file diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index dd1495265..400333d51 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -36,6 +36,7 @@ namespace Squidex.Config.Domain settings.ContractResolver = new ConverterContractResolver( new AppClientsConverter(), new AppContributorsConverter(), + new ClaimsPrincipalConverter(), new InstantConverter(), new LanguageConverter(), new LanguagesConfigConverter(), diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs index 1614dc8ef..a3fb1aafa 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/LanguagesConfigTests.cs @@ -172,14 +172,6 @@ namespace Squidex.Domain.Apps.Core.Model.Apps config.Remove(Language.EN); } - [Fact] - public void Should_throw_exception_if_language_to_remove_is_master_language() - { - var config = LanguagesConfig.Build(Language.DE); - - Assert.Throws(() => config.Remove(Language.DE)); - } - [Fact] public void Should_update_language() { diff --git a/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs new file mode 100644 index 000000000..727c7e2c6 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/ClaimsPrincipalConverterTests.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// ClaimsPrincipalConverterTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ClaimsPrincipalConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = new ClaimsPrincipal( + new[] + { + new ClaimsIdentity( + new[] + { + new Claim("email", "me@email.de"), + new Claim("username", "me@email.de"), + }, + "Cookie"), + new ClaimsIdentity( + new[] + { + new Claim("user_id", "12345"), + new Claim("login", "me"), + }, + "Google") + }); + + var result = value.SerializeAndDeserializeAndReturn(new ClaimsPrincipalConverter()); + + Assert.Equal(value.Identities.ElementAt(0).AuthenticationType, result.Identities.ElementAt(0).AuthenticationType); + Assert.Equal(value.Identities.ElementAt(1).AuthenticationType, result.Identities.ElementAt(1).AuthenticationType); + } + + [Fact] + public void Should_serialize_and_deserialize_null_principal() + { + ClaimsPrincipal value = null; + + value.SerializeAndDeserialize(new ClaimsPrincipalConverter()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs index 59d9492be..5db717293 100644 --- a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -22,5 +22,21 @@ namespace Squidex.Infrastructure.Json value.SerializeAndDeserialize(new InstantConverter()); } + + [Fact] + public void Should_serialize_and_deserialize_nullable_with_value() + { + Instant? value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new InstantConverter()); + } + + [Fact] + public void Should_serialize_and_deserialize_nullable_with_null() + { + Instant? value = null; + + value.SerializeAndDeserialize(new InstantConverter()); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index feab2ba46..c4b900f36 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -42,18 +42,6 @@ namespace Squidex.Infrastructure Assert.True(Language.AllLanguages.Count() > 100); } - [Fact] - public void Should_serialize_and_deserialize_null_language() - { - JsonHelper.SerializeAndDeserialize(null, new LanguageConverter()); - } - - [Fact] - public void Should_serialize_and_deserialize_valid_language() - { - Language.DE.SerializeAndDeserialize(new LanguageConverter()); - } - [Fact] public void Should_return_true_for_valid_language() { @@ -120,5 +108,21 @@ namespace Squidex.Infrastructure Assert.Null(language); } + + [Fact] + public void Should_serialize_and_deserialize_null_language() + { + Language value = null; + + value.SerializeAndDeserialize(new LanguageConverter()); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_language() + { + var value = Language.DE; + + value.SerializeAndDeserialize(new LanguageConverter()); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs b/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs index 04ca98112..5da1c9701 100644 --- a/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs +++ b/tests/Squidex.Infrastructure.Tests/NamedIdTests.cs @@ -90,37 +90,49 @@ namespace Squidex.Infrastructure [Fact] public void Should_serialize_and_deserialize_null_guid_token() { - JsonHelper.SerializeAndDeserialize>(null, new NamedGuidIdConverter()); + NamedId value = null; + + value.SerializeAndDeserialize(new NamedGuidIdConverter()); } [Fact] public void Should_serialize_and_deserialize_valid_guid_token() { - new NamedId(Guid.NewGuid(), "my-name").SerializeAndDeserialize(new NamedGuidIdConverter()); + var value = new NamedId(Guid.NewGuid(), "my-name"); + + value.SerializeAndDeserialize(new NamedGuidIdConverter()); } [Fact] public void Should_serialize_and_deserialize_null_long_token() { - JsonHelper.SerializeAndDeserialize>(null, new NamedLongIdConverter()); + NamedId value = null; + + value.SerializeAndDeserialize(new NamedLongIdConverter()); } [Fact] public void Should_serialize_and_deserialize_valid_long_token() { - new NamedId(123, "my-name").SerializeAndDeserialize(new NamedLongIdConverter()); + var value = new NamedId(123, "my-name"); + + value.SerializeAndDeserialize(new NamedLongIdConverter()); } [Fact] public void Should_serialize_and_deserialize_null_string_token() { - JsonHelper.SerializeAndDeserialize>(null, new NamedStringIdConverter()); + NamedId value = null; + + value.SerializeAndDeserialize(new NamedStringIdConverter()); } [Fact] public void Should_serialize_and_deserialize_valid_string_token() { - new NamedId(Guid.NewGuid().ToString(), "my-name").SerializeAndDeserialize(new NamedStringIdConverter()); + var value = new NamedId(Guid.NewGuid().ToString(), "my-name"); + + value.SerializeAndDeserialize(new NamedStringIdConverter()); } [Fact] diff --git a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs index 96dfd765e..3fea6b2c4 100644 --- a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -108,13 +108,17 @@ namespace Squidex.Infrastructure [Fact] public void Should_serialize_and_deserialize_null_token() { - JsonHelper.SerializeAndDeserialize(null, new RefTokenConverter()); + RefToken value = null; + + value.SerializeAndDeserialize(new RefTokenConverter()); } [Fact] public void Should_serialize_and_deserialize_valid_token() { - RefToken.Parse("client:client1").SerializeAndDeserialize(new RefTokenConverter()); + var value = RefToken.Parse("client:client1"); + + value.SerializeAndDeserialize(new RefTokenConverter()); } } } diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index d1585a0dc..394475828 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -30,6 +30,13 @@ namespace Squidex.Infrastructure.TestHelpers } public static void SerializeAndDeserialize(this T value, JsonConverter converter) + { + var output = SerializeAndDeserializeAndReturn(value, converter); + + Assert.Equal(value, output); + } + + public static T SerializeAndDeserializeAndReturn(this T value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); @@ -39,7 +46,7 @@ namespace Squidex.Infrastructure.TestHelpers var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); var output = JsonConvert.DeserializeObject>(result, serializerSettings); - Assert.Equal(value, output.Item1); + return output.Item1; } public static void DoesNotDeserialize(string value, JsonConverter converter)