diff --git a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs index f709c24f1..e855d1c21 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleTriggerValidator.cs @@ -42,23 +42,52 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards public Task> Visit(UsageTrigger trigger) { - return Task.FromResult(Enumerable.Empty()); + var errors = new List(); + + if (trigger.NumDays.HasValue && (trigger.NumDays < 1 || trigger.NumDays > 30)) + { + errors.Add(new ValidationError("Num days must be between 1 and 30.", nameof(trigger.NumDays))); + } + + return Task.FromResult< IEnumerable>(errors); } public async Task> Visit(ContentChangedTriggerV2 trigger) { + var errors = new List(); + if (trigger.Schemas != null) { - var schemaErrors = await Task.WhenAll( - trigger.Schemas.Select(async s => - await SchemaProvider(s.SchemaId) == null - ? new ValidationError($"Schema {s.SchemaId} does not exist.", nameof(trigger.Schemas)) - : null)); + var tasks = new List>(); + + foreach (var schema in trigger.Schemas) + { + if (schema.SchemaId == Guid.Empty) + { + errors.Add(new ValidationError("Schema id is required.", nameof(trigger.Schemas))); + } + else + { + tasks.Add(CheckSchemaAsync(schema)); + } + } - return schemaErrors.Where(x => x != null).ToList(); + var checkErrors = await Task.WhenAll(tasks); + + errors.AddRange(checkErrors.Where(x => x != null)); + } + + return errors; + } + + private async Task CheckSchemaAsync(ContentChangedTriggerSchemaV2 schema) + { + if (await SchemaProvider(schema.SchemaId) == null) + { + return new ValidationError($"Schema {schema.SchemaId} does not exist.", nameof(ContentChangedTriggerV2.Schemas)); } - return new List(); + return null; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index 3eec572b0..a0cb3b57b 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -24,7 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking [Reentrant] public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain { - private const int MaxDays = 30; private readonly IUsageTracker usageTracker; public sealed class Target @@ -110,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) { - if (numDays > 0 && numDays < MaxDays) + if (numDays.HasValue) { return (today.AddDays(-numDays.Value).AddDays(1), today); } diff --git a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs index 97e24e087..e5dbce7bc 100644 --- a/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Rules/Models/Triggers/UsageRuleTriggerDto.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.ComponentModel.DataAnnotations; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Infrastructure.Reflection; @@ -21,6 +22,7 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Triggers /// /// The number of days to check or null for the current month. /// + [Range(1, 30)] public int? NumDays { get; set; } public override RuleTrigger ToTrigger() diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html index 9b18d5467..d2637ac1f 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -2,12 +2,24 @@
- + - + The monthly api calls to trigger.
+ +
+ + + + + + + + The number of days to check or empty to check the current month. + +
\ No newline at end of file diff --git a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts index d08c1d15c..6fa94dd64 100644 --- a/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts +++ b/src/Squidex/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -6,7 +6,9 @@ */ import { Component, Input, OnInit } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { ValidatorsEx } from '@app/shared'; @Component({ selector: 'sqx-usage-trigger', @@ -25,6 +27,13 @@ export class UsageTriggerComponent implements OnInit { public ngOnInit() { this.triggerForm.setControl('limit', - new FormControl(this.trigger.limit || 20000)); + new FormControl(this.trigger.limit || 20000, [ + Validators.required + ])); + + this.triggerForm.setControl('numDays', + new FormControl(this.trigger.numDays, [ + ValidatorsEx.between(1, 30) + ])); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html index 48ea86ad0..fdd06b41c 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-preview-urls-form.component.html @@ -33,13 +33,13 @@
- +
- + diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index caeb9e437..86a8fed4e 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -15,8 +15,8 @@ const DEFAULT_ERRORS: { [key: string]: string } = { required: '{field} is required.', pattern: '{field} does not follow the pattern.', patternmessage: '{message}', - minvalue: '{field} must be larger than {minValue}.', - maxvalue: '{field} must be smaller than {maxValue}.', + minvalue: '{field} must be larger or equals to {minValue}.', + maxvalue: '{field} must be smaller or equals to {maxValue}.', minmax: '{field} must have a length of more than {requiredLength}.', maxlength: '{field} must have a length of less than {requiredLength}.', match: '{message}', diff --git a/src/Squidex/app/framework/angular/forms/validators.spec.ts b/src/Squidex/app/framework/angular/forms/validators.spec.ts index 5a9126443..51d538b51 100644 --- a/src/Squidex/app/framework/angular/forms/validators.spec.ts +++ b/src/Squidex/app/framework/angular/forms/validators.spec.ts @@ -26,6 +26,22 @@ describe('ValidatorsEx.between', () => { expect(error).toBeNull(); }); + it('should return null when value is null', () => { + const input = new FormControl(null); + + const error = ValidatorsEx.between(1, 5)(input); + + expect(error).toBeNull(); + }); + + it('should return null when value is undefined', () => { + const input = new FormControl(undefined); + + const error = ValidatorsEx.between(1, 5)(input); + + expect(error).toBeNull(); + }); + it('should return error when not a number', () => { const input = new FormControl('text'); diff --git a/src/Squidex/app/framework/angular/forms/validators.ts b/src/Squidex/app/framework/angular/forms/validators.ts index 32c6557bf..84ca3959b 100644 --- a/src/Squidex/app/framework/angular/forms/validators.ts +++ b/src/Squidex/app/framework/angular/forms/validators.ts @@ -97,12 +97,14 @@ export module ValidatorsEx { return (control: AbstractControl) => { const value: number = control.value; - if (!Types.isNumber(value)) { - return { validnumber: false }; - } else if (minValue && value < minValue) { - return { minvalue: { minValue, actualValue: value } }; - } else if (maxValue && value > maxValue) { - return { maxvalue: { maxValue, actualValue: value } }; + if (!Types.isUndefined(value) && !Types.isNull(value)) { + if (!Types.isNumber(value)) { + return { validnumber: false }; + } else if (minValue && value < minValue) { + return { minvalue: { minValue, actualValue: value } }; + } else if (maxValue && value > maxValue) { + return { maxvalue: { maxValue, actualValue: value } }; + } } return null; diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs index 5d386c6ac..a4eb93efd 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs @@ -6,10 +6,13 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; +using FluentAssertions; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Xunit; @@ -19,23 +22,46 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers { private readonly IAppProvider appProvider = A.Fake(); private readonly Guid appId = Guid.NewGuid(); + private readonly Guid schemaId = Guid.NewGuid(); [Fact] - public async Task Should_add_error_if_schemas_ids_are_not_valid() + public async Task Should_add_error_if_schema_id_is_not_defined() { + var trigger = new ContentChangedTriggerV2 + { + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2()) + }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Schema id is required.", "Schemas") + }); + A.CallTo(() => appProvider.GetSchemaAsync(appId, A.Ignored, false)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_add_error_if_schemas_ids_are_not_valid() + { + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTriggerV2 { - Schemas = ReadOnlyCollection.Create( - new ContentChangedTriggerSchemaV2() - ) + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId }) }; var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); - Assert.NotEmpty(errors); + errors.Should().BeEquivalentTo( + new List + { + new ValidationError($"Schema {schemaId} does not exist.", "Schemas") + }); } [Fact] @@ -69,9 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers var trigger = new ContentChangedTriggerV2 { - Schemas = ReadOnlyCollection.Create( - new ContentChangedTriggerSchemaV2() - ) + Schemas = ReadOnlyCollection.Create(new ContentChangedTriggerSchemaV2 { SchemaId = schemaId }) }; var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs new file mode 100644 index 000000000..827d8a8b4 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/UsageTriggerValidationTests.cs @@ -0,0 +1,72 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Rules.Triggers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Guards.Triggers +{ + public class UsageTriggerValidationTests + { + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + + [Fact] + public async Task Should_add_error_if_num_days_less_than_1() + { + var trigger = new UsageTrigger { NumDays = 0 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } + + [Fact] + public async Task Should_add_error_if_num_days_greater_than_30() + { + var trigger = new UsageTrigger { NumDays = 32 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Num days must be between 1 and 30.", "NumDays") + }); + } + + [Fact] + public async Task Should_not_add_error_if_num_days_is_valid() + { + var trigger = new UsageTrigger { NumDays = 20 }; + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_num_days_is_not_defined() + { + var trigger = new UsageTrigger(); + + var errors = await RuleTriggerValidator.ValidateAsync(appId, trigger, appProvider); + + Assert.Empty(errors); + } + } +}