From eb82bcc16cb3ffe5f20d09d3fd17ebf98e988b78 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Oct 2017 11:23:23 +0200 Subject: [PATCH] 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); + } + } +}