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