diff --git a/Squidex.ruleset b/Squidex.ruleset new file mode 100644 index 000000000..78388c901 --- /dev/null +++ b/Squidex.ruleset @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 34ec3dc16..dd880d5ac 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -1,50 +1,7 @@  - True - False - True - False - True - False - True - True - True - True - True - True - True - True - True - False - True - DO_NOT_SHOW - - True - DO_NOT_SHOW - WARNING - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - WARNING - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - DO_NOT_SHOW - WARNING - False - True - - ExplicitlyExcluded - TypeScript16 <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> - - SingleQuoted ========================================================================== $FILENAME$ Squidex Headless CMS @@ -53,52 +10,4 @@ All rights reserved. ========================================================================== - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - C:\Users\mail2\AppData\Local\JetBrains\Transient\ReSharperPlatformVs15\v08_85ffde88\SolutionCaches - True - True - True - True - True \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs new file mode 100644 index 000000000..ee44aac44 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/ConfigAppPlansProvider.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// ConfigAppLimitsProvider.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations +{ + public sealed class ConfigAppPlansProvider : IAppPlansProvider + { + private static readonly ConfigAppLimitsPlan Infinite = new ConfigAppLimitsPlan + { + Id = "infinite", + Name = "Infinite", + MaxApiCalls = -1, + MaxAssetSize = -1, + MaxContributors = -1 + }; + + private readonly Dictionary config; + + public ConfigAppPlansProvider(IEnumerable config) + { + Guard.NotNull(config, nameof(config)); + + this.config = config.Select(c => c.Clone()).OrderBy(x => x.MaxApiCalls).ToDictionary(c => c.Id, StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable GetAvailablePlans() + { + return config.Values; + } + + public IAppLimitsPlan GetPlanForApp(IAppEntity app) + { + Guard.NotNull(app, nameof(app)); + + return GetPlan(app.PlanId); + } + + public IAppLimitsPlan GetPlan(string planId) + { + return config.GetOrDefault(planId ?? string.Empty) ?? config.Values.FirstOrDefault() ?? Infinite; + } + + public bool IsConfiguredPlan(string planId) + { + return planId != null && config.ContainsKey(planId); + } + } +} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs new file mode 100644 index 000000000..4ad558c5c --- /dev/null +++ b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationFieldPropertiesDto.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// GeolocationPropertiesDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using NJsonSchema.Annotations; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Schemas.Models +{ + [JsonSchema("Geolocation")] + public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto + { + /// + /// The default value for the field value. + /// + public bool? DefaultValue { get; set; } + + /// + /// The editor that is used to manage this field. + /// + [JsonConverter(typeof(StringEnumConverter))] + public GeolocationFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new GeolocationFieldProperties()); + + return result; + } + } +} diff --git a/src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs b/src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs new file mode 100644 index 000000000..62c345510 --- /dev/null +++ b/src/Squidex/Controllers/ContentApi/Models/AssetsDto.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// ContentsDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Controllers.ContentApi.Models +{ + public sealed class AssetsDto + { + /// + /// The total number of content items. + /// + public long Total { get; set; } + + /// + /// The content items. + /// + public ContentDto[] Items { get; set; } + } +} diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldPropertiesTests.cs new file mode 100644 index 000000000..cefda1812 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Schemas/GeolocationFieldPropertiesTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// GeolocationPropertiesTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public class GeolocationFieldPropertiesTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; + + sut.Validate(errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor is not a valid value", "Editor") + }); + } + + [Fact] + public void Should_set_or_freeze_sut() + { + var sut = new GeolocationFieldProperties(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + property.SetValue(sut, value); + + var result = property.GetValue(sut); + + Assert.Equal(value, result); + } + + sut.Freeze(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + Assert.Throws(() => + { + try + { + property.SetValue(sut, value); + } + catch (Exception ex) + { + throw ex.InnerException; + } + }); + } + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs new file mode 100644 index 000000000..2ea48e53d --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Assets/AssetStoreTests.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// AssetStoreTestsBase.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +// ReSharper disable VirtualMemberCallInConstructor +// ReSharper disable MemberCanBeProtected.Global + +namespace Squidex.Infrastructure.Assets +{ + public abstract class AssetStoreTests : IDisposable where T : IAssetStore + { + private readonly T sut; + + protected AssetStoreTests() + { + sut = CreateStore(); + } + + protected T Sut + { + get { return sut; } + } + + public abstract T CreateStore(); + + public abstract void Dispose(); + + [Fact] + public Task Should_throw_exception_if_asset_to_download_is_not_found() + { + ((IExternalSystem)Sut).Connect(); + + return Assert.ThrowsAsync(() => Sut.DownloadAsync(Id(), 1, "suffix", new MemoryStream())); + } + + [Fact] + public Task Should_throw_exception_if_asset_to_copy_is_not_found() + { + ((IExternalSystem)Sut).Connect(); + + return Assert.ThrowsAsync(() => Sut.CopyTemporaryAsync(Id(), Id(), 1, null)); + } + + [Fact] + public async Task Should_read_and_write_file() + { + ((IExternalSystem)Sut).Connect(); + + var assetId = Id(); + var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + + await Sut.UploadAsync(assetId, 1, "suffix", assetData); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(assetId, 1, "suffix", readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_commit_temporary_file() + { + ((IExternalSystem)Sut).Connect(); + + var tempId = Id(); + + var assetId = Id(); + var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + + await Sut.UploadTemporaryAsync(tempId, assetData); + await Sut.CopyTemporaryAsync(tempId, assetId, 1, "suffix"); + + var readData = new MemoryStream(); + + await Sut.DownloadAsync(assetId, 1, "suffix", readData); + + Assert.Equal(assetData.ToArray(), readData.ToArray()); + } + + [Fact] + public async Task Should_ignore_when_deleting_twice() + { + ((IExternalSystem)Sut).Connect(); + + var tempId = Id(); + + var assetData = new MemoryStream(new byte[] { 0x1, 0x2, 0x3, 0x4 }); + + await Sut.UploadTemporaryAsync(tempId, assetData); + await Sut.DeleteTemporaryAsync(tempId); + await Sut.DeleteTemporaryAsync(tempId); + } + + private static string Id() + { + return Guid.NewGuid().ToString(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampCommandMiddlewareTests.cs new file mode 100644 index 000000000..bb09751b9 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -0,0 +1,53 @@ +// ========================================================================== +// EnrichWithTimestampCommandMiddlewareTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Xunit; + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public sealed class EnrichWithTimestampCommandMiddlewareTests + { + private sealed class MyTimestampCommand : ITimestampCommand + { + public Instant Timestamp { get; set; } + + public long? ExpectedVersion { get; set; } + } + + private readonly IClock clock = A.Fake(); + + [Fact] + public async Task Should_set_timestamp_for_timestamp_command() + { + var utc = Instant.FromUnixTimeSeconds(1000); + var sut = new EnrichWithTimestampCommandMiddleware(clock); + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(utc); + + var command = new MyTimestampCommand(); + + await sut.HandleAsync(new CommandContext(command)); + + Assert.Equal(utc, command.Timestamp); + } + + [Fact] + public async Task Should_do_nothing_for_normal_command() + { + var sut = new EnrichWithTimestampCommandMiddleware(clock); + + await sut.HandleAsync(new CommandContext(A.Dummy())); + + A.CallTo(() => clock.GetCurrentInstant()).MustNotHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogCommandMiddlewareTests.cs new file mode 100644 index 000000000..992f2f98c --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogCommandMiddlewareTests.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// LogExceptionHandlerTests.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.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public class LogCommandMiddlewareTests + { + private readonly MyLog log = new MyLog(); + private readonly LogCommandMiddleware sut; + private readonly ICommand command = A.Dummy(); + + private sealed class MyLog : ISemanticLog + { + public int LogCount { get; private set; } + + public Dictionary LogLevels { get; } = new Dictionary(); + + public void Log(SemanticLogLevel logLevel, Action action) + { + LogCount++; + LogLevels[logLevel] = LogLevels.GetOrDefault(logLevel) + 1; + } + + public ISemanticLog CreateScope(Action objectWriter) + { + throw new NotSupportedException(); + } + } + + public LogCommandMiddlewareTests() + { + sut = new LogCommandMiddleware(log); + } + + [Fact] + public async Task Should_log_before_and_after_request() + { + var context = new CommandContext(command); + + await sut.HandleAsync(context, () => + { + context.Complete(true); + + return TaskHelper.Done; + }); + + Assert.Equal(3, log.LogCount); + Assert.Equal(3, log.LogLevels[SemanticLogLevel.Information]); + } + + [Fact] + public async Task Should_log_error_if_command_failed() + { + var context = new CommandContext(command); + + await Assert.ThrowsAsync(async () => + { + await sut.HandleAsync(context, () => throw new InvalidOperationException()); + }); + + Assert.Equal(3, log.LogCount); + Assert.Equal(2, log.LogLevels[SemanticLogLevel.Information]); + Assert.Equal(1, log.LogLevels[SemanticLogLevel.Error]); + } + + [Fact] + public async Task Should_log_if_command_is_not_handled() + { + var context = new CommandContext(command); + + await sut.HandleAsync(context, () => TaskHelper.Done); + + Assert.Equal(4, log.LogCount); + Assert.Equal(3, log.LogLevels[SemanticLogLevel.Information]); + Assert.Equal(1, log.LogLevels[SemanticLogLevel.Fatal]); + } + } +} \ No newline at end of file