diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index 11b072b8c..e67652f9c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var pathString = path.ToPathString(); - errors.Add(new ValidationError($"{pathString}: {message}", pathString)); + errors.Add(new ValidationError(message, pathString)); } public Task ValidatePartialAsync(NamedContentData data) diff --git a/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs new file mode 100644 index 000000000..b7edc178c --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs @@ -0,0 +1,48 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Log; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class LoggingFilter : IIncomingGrainCallFilter + { + private readonly ISemanticLog log; + + public LoggingFilter(ISemanticLog log) + { + Guard.NotNull(log, nameof(log)); + + this.log = log; + } + + public async Task Invoke(IIncomingGrainCallContext context) + { + try + { + await context.Invoke(); + } + catch (DomainException) + { + throw; + } + catch (Exception ex) + { + log.LogError(ex, w => w + .WriteProperty("action", "GrainInvoked") + .WriteProperty("status", "Failed") + .WriteProperty("grain", context.Grain.ToString()) + .WriteProperty("grainMethod", context.ImplementationMethod.ToString())); + + throw; + } + } + } +} diff --git a/src/Squidex.Infrastructure/ValidationException.cs b/src/Squidex.Infrastructure/ValidationException.cs index 16f3d836c..3f1b49c1e 100644 --- a/src/Squidex.Infrastructure/ValidationException.cs +++ b/src/Squidex.Infrastructure/ValidationException.cs @@ -78,18 +78,21 @@ namespace Squidex.Infrastructure for (var i = 0; i < errors.Count; i++) { - var error = errors[i].Message; + var error = errors[i]?.Message; - sb.Append(error); - - if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrWhiteSpace(error)) { - sb.Append("."); - } + sb.Append(error); - if (i < errors.Count - 1) - { - sb.Append(" "); + if (!error.EndsWith(".", StringComparison.OrdinalIgnoreCase)) + { + sb.Append("."); + } + + if (i < errors.Count - 1) + { + sb.Append(" "); + } } } } diff --git a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs index 2dc98b9bb..7c0546ae5 100644 --- a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -5,8 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.IO; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -15,10 +15,7 @@ namespace Squidex.Areas.Frontend.Middlewares { public sealed class WebpackMiddleware { - private const string Host = "localhost"; - private const string Port = "3000"; - private static readonly string[] Scripts = { "shims", "app" }; - private static readonly string[] Styles = Array.Empty(); + private const string WebpackUrl = "http://localhost:3000/index.html"; private readonly RequestDelegate next; public WebpackMiddleware(RequestDelegate next) @@ -28,7 +25,28 @@ namespace Squidex.Areas.Frontend.Middlewares public async Task Invoke(HttpContext context) { - if (context.IsHtmlPath()) + if (context.IsIndex()) + { + if (context.Response.StatusCode != 304) + { + using (var client = new HttpClient()) + { + var result = await client.GetAsync(WebpackUrl); + + context.Response.StatusCode = (int)result.StatusCode; + + if (result.IsSuccessStatusCode) + { + var html = await result.Content.ReadAsStringAsync(); + + html = AdjustBase(html, context.Request.PathBase); + + await context.Response.WriteHtmlAsync(html); + } + } + } + } + else if (context.IsHtmlPath()) { var responseBuffer = new MemoryStream(); var responseBody = context.Response.Body; @@ -39,25 +57,14 @@ namespace Squidex.Areas.Frontend.Middlewares context.Response.Body = responseBody; - var response = Encoding.UTF8.GetString(responseBuffer.ToArray()); + var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); - if (context.IsIndex()) - { - response = InjectStyles(response); - response = InjectScripts(response); - } - - var basePath = context.Request.PathBase; - - if (basePath.HasValue) - { - response = AdjustBase(response, basePath.Value); - } + html = AdjustBase(html, context.Request.PathBase); - context.Response.ContentLength = Encoding.UTF8.GetByteCount(response); + context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); context.Response.Body = responseBody; - await context.Response.WriteAsync(response); + await context.Response.WriteAsync(html); } else { @@ -65,47 +72,16 @@ namespace Squidex.Areas.Frontend.Middlewares } } - private static string InjectStyles(string response) + private static string AdjustBase(string html, PathString baseUrl) { - if (!response.Contains("")) + if (baseUrl.HasValue) { - return response; + return html.Replace("", $""); } - - var sb = new StringBuilder(); - - foreach (var file in Styles) - { - sb.AppendLine($""); - } - - response = response.Replace("", $"{sb}"); - - return response; - } - - private static string InjectScripts(string response) - { - if (!response.Contains("")) - { - return response; - } - - var sb = new StringBuilder(); - - foreach (var file in Scripts) + else { - sb.AppendLine($""); + return html; } - - response = response.Replace("", $"{sb}"); - - return response; - } - - private static string AdjustBase(string response, string baseUrl) - { - return response.Replace("", $""); } } } diff --git a/src/Squidex/Config/Logging.cs b/src/Squidex/Config/Logging.cs index 76a2b22ea..42e3a0ecc 100644 --- a/src/Squidex/Config/Logging.cs +++ b/src/Squidex/Config/Logging.cs @@ -14,7 +14,7 @@ namespace Squidex.Config { public static class Logging { - public static void AddFilter(this ILoggingBuilder builder) + public static void AddFilters(this ILoggingBuilder builder) { builder.AddFilter((category, level) => { diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs index 458dc225a..ace29d8ee 100644 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/src/Squidex/Config/Orleans/OrleansServices.cs @@ -28,9 +28,15 @@ namespace Squidex.Config.Orleans { services.AddOrleans(config, environment, builder => { + builder.ConfigureLogging(logging => + { + logging.AddFilters(); + }); + builder.ConfigureServices(siloServices => { siloServices.AddSingleton(); + siloServices.AddSingleton(); }); builder.ConfigureApplicationParts(parts => diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs index e427eff3d..dd6f6abd7 100644 --- a/src/Squidex/Program.cs +++ b/src/Squidex/Program.cs @@ -31,7 +31,7 @@ namespace Squidex { builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); builder.AddSemanticLog(); - builder.AddFilter(); + builder.AddFilters(); }) .ConfigureAppConfiguration((hostContext, builder) => { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index d6b26b049..1c1601934 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("unknown: Not a known field.", "unknown") + new ValidationError("Not a known field.", "unknown") }); } @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field: Must be less or equal to '100'.", "my-field") + new ValidationError("Must be less or equal to '100'.", "my-field") }); } @@ -80,8 +80,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(es): Not a known invariant value.", "my-field(es)"), - new ValidationError("my-field(it): Not a known invariant value.", "my-field(it)") + new ValidationError("Not a known invariant value.", "my-field(es)"), + new ValidationError("Not a known invariant value.", "my-field(it)") }); } @@ -99,8 +99,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(de): Field is required.", "my-field(de)"), - new ValidationError("my-field(en): Field is required.", "my-field(en)") + new ValidationError("Field is required.", "my-field(de)"), + new ValidationError("Field is required.", "my-field(en)") }); } @@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field: Field is required.", "my-field") + new ValidationError("Field is required.", "my-field") }); } @@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(xx): Not a known language.", "my-field(xx)") + new ValidationError("Not a known language.", "my-field(xx)") }); } @@ -182,8 +182,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(es): Not a known language.", "my-field(es)"), - new ValidationError("my-field(it): Not a known language.", "my-field(it)") + new ValidationError("Not a known language.", "my-field(es)"), + new ValidationError("Not a known language.", "my-field(it)") }); } @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("unknown: Not a known field.", "unknown") + new ValidationError("Not a known field.", "unknown") }); } @@ -221,7 +221,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field: Must be less or equal to '100'.", "my-field") + new ValidationError("Must be less or equal to '100'.", "my-field") }); } @@ -242,8 +242,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(es): Not a known invariant value.", "my-field(es)"), - new ValidationError("my-field(it): Not a known invariant value.", "my-field(it)") + new ValidationError("Not a known invariant value.", "my-field(es)"), + new ValidationError("Not a known invariant value.", "my-field(it)") }); } @@ -292,7 +292,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(xx): Not a known language.", "my-field(xx)") + new ValidationError("Not a known language.", "my-field(xx)") }); } @@ -313,8 +313,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field(es): Not a known language.", "my-field(es)"), - new ValidationError("my-field(it): Not a known language.", "my-field(it)") + new ValidationError("Not a known language.", "my-field(es)"), + new ValidationError("Not a known language.", "my-field(it)") }); } @@ -340,8 +340,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent errors.Should().BeEquivalentTo( new List { - new ValidationError("my-field[1].my-nested: Field is required.", "my-field[1].my-nested"), - new ValidationError("my-field[3].my-nested: Field is required.", "my-field[3].my-nested") + new ValidationError("Field is required.", "my-field[1].my-nested"), + new ValidationError("Field is required.", "my-field[3].my-nested") }); } } diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs new file mode 100644 index 000000000..28ee105e9 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Squidex.Infrastructure.Log; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class LoggingFilterTests + { + private readonly ISemanticLog log = A.Fake(); + private readonly IIncomingGrainCallContext context = A.Fake(); + private readonly LoggingFilter sut; + + public LoggingFilterTests() + { + sut = new LoggingFilter(log); + } + + [Fact] + public async Task Should_not_log_if_no_exception_happened() + { + await sut.Invoke(context); + + A.CallTo(() => log.Log(A.Ignored, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_log_domain_exceptions() + { + A.CallTo(() => context.Invoke()) + .Throws(new ValidationException("Failed")); + + await Assert.ThrowsAsync(() => sut.Invoke(context)); + + A.CallTo(() => log.Log(A.Ignored, A.Ignored, A>.Ignored)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_log_exception_and_forward_it() + { + A.CallTo(() => context.Invoke()) + .Throws(new InvalidOperationException()); + + await Assert.ThrowsAsync(() => sut.Invoke(context)); + + A.CallTo(() => log.Log(A.Ignored, A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs b/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs index 9acb88b7a..8d2df1fbc 100644 --- a/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/ValidationExceptionTests.cs @@ -53,6 +53,18 @@ namespace Squidex.Infrastructure Assert.Equal("Summary: Error1. Error2.", ex.Message); } + [Fact] + public void Should_serialize_and_deserialize1() + { + var source = new ValidationException("Summary", new ValidationError("Error1"), null); + var result = source.SerializeAndDeserializeBinary(); + + result.Errors.Should().BeEquivalentTo(source.Errors); + + Assert.Equal(source.Message, result.Message); + Assert.Equal(source.Summary, result.Summary); + } + [Fact] public void Should_serialize_and_deserialize() {