From 1823a1cb035a5ecda88d7d6981f61886df7aa03f Mon Sep 17 00:00:00 2001 From: Alex Van Dyke Date: Mon, 5 Mar 2018 15:08:07 -0600 Subject: [PATCH] Transfering pipeline tests --- Squidex.sln | 16 ++- src/Squidex/Pipeline/ApiCostsFilter.cs | 4 + src/Squidex/Pipeline/AppApiFilter.cs | 2 +- tests/Squidex.Tests/GlobalSuppressions.cs | 14 ++ .../Pipeline/ActionContextLogAppenderTests.cs | 86 ++++++++++++ .../Pipeline/ApiAuthorizeAttributeTests.cs | 32 +++++ tests/Squidex.Tests/Pipeline/ApiCostTests.cs | 104 ++++++++++++++ .../ApiExceptionFilterAttributeTests.cs | 101 ++++++++++++++ tests/Squidex.Tests/Pipeline/AppApiTests.cs | 90 ++++++++++++ .../Pipeline/AppPermissionAttributeTests.cs | 130 ++++++++++++++++++ .../ETagCommandMiddlewareTests.cs | 46 +++++++ .../EnrichWithActorCommandMiddlewareTests.cs | 85 ++++++++++++ .../EnrichWithAppIdCommandMiddlewareTests.cs | 65 +++++++++ ...nrichWithSchemaIdCommandMiddlewareTests.cs | 124 +++++++++++++++++ .../Pipeline/Swagger/SwaggerHelperTests.cs | 120 ++++++++++++++++ tests/Squidex.Tests/Squidex.Tests.csproj | 39 ++++++ 16 files changed, 1056 insertions(+), 2 deletions(-) create mode 100644 tests/Squidex.Tests/GlobalSuppressions.cs create mode 100644 tests/Squidex.Tests/Pipeline/ActionContextLogAppenderTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/ApiAuthorizeAttributeTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/ApiCostTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/ApiExceptionFilterAttributeTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/AppApiTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/AppPermissionAttributeTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs create mode 100644 tests/Squidex.Tests/Pipeline/Swagger/SwaggerHelperTests.cs create mode 100644 tests/Squidex.Tests/Squidex.Tests.csproj diff --git a/Squidex.sln b/Squidex.sln index e4671fde7..67ffac379 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 +VisualStudioVersion = 15.0.27130.2036 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}" EndProject @@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Domain.Apps.Entitie EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{A4823E14-C0E5-4A4D-B28F-27424C25C3C7}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squidex.Tests\Squidex.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -307,6 +309,18 @@ Global {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x64.Build.0 = Release|Any CPU {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x86.ActiveCfg = Release|Any CPU {A4823E14-C0E5-4A4D-B28F-27424C25C3C7}.Release|x86.Build.0 = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|x64.ActiveCfg = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|x64.Build.0 = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|x86.ActiveCfg = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Debug|x86.Build.0 = Debug|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|Any CPU.Build.0 = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x64.ActiveCfg = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x64.Build.0 = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.ActiveCfg = Release|Any CPU + {7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Squidex/Pipeline/ApiCostsFilter.cs b/src/Squidex/Pipeline/ApiCostsFilter.cs index 842915238..a6e14234c 100644 --- a/src/Squidex/Pipeline/ApiCostsFilter.cs +++ b/src/Squidex/Pipeline/ApiCostsFilter.cs @@ -35,6 +35,10 @@ namespace Squidex.Pipeline { return (ApiCostsAttribute)((IFilterContainer)this).FilterDefinition; } + set + { + ((IFilterContainer)this).FilterDefinition = value; + } } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) diff --git a/src/Squidex/Pipeline/AppApiFilter.cs b/src/Squidex/Pipeline/AppApiFilter.cs index 0750a7186..4c65a1f42 100644 --- a/src/Squidex/Pipeline/AppApiFilter.cs +++ b/src/Squidex/Pipeline/AppApiFilter.cs @@ -17,7 +17,7 @@ namespace Squidex.Pipeline { private readonly IAppProvider appProvider; - private sealed class AppFeature : IAppFeature + public class AppFeature : IAppFeature { public IAppEntity App { get; } diff --git a/tests/Squidex.Tests/GlobalSuppressions.cs b/tests/Squidex.Tests/GlobalSuppressions.cs new file mode 100644 index 000000000..8f1cbb214 --- /dev/null +++ b/tests/Squidex.Tests/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// GlobalSuppressions.cs +// CivicPlus implementation of Squidex Headless CMS +// ========================================================================== +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File must have header", Justification = "CP Has their own headers.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "CP Has their own headers.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1200:Using directives must be placed correctly", Justification = "Usings should be outside namespace")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "this. prefix isn't necessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:Split parameters must start on line after declaration", Justification = "Not necessary")] \ No newline at end of file diff --git a/tests/Squidex.Tests/Pipeline/ActionContextLogAppenderTests.cs b/tests/Squidex.Tests/Pipeline/ActionContextLogAppenderTests.cs new file mode 100644 index 000000000..189896e24 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/ActionContextLogAppenderTests.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Infrastructure.Log; +using Squidex.Pipeline; +using Xunit; + +namespace Squidex.Tests.Pipeline +{ + public class ActionContextLogAppenderTests + { + private readonly Mock actionContextAccessor = new Mock(); + private readonly Mock httpContextMock = new Mock(); + private readonly Mock actionDescriptor = new Mock(); + private readonly RouteData routeData = new RouteData(); + private readonly Guid requestId = Guid.NewGuid(); + private readonly IDictionary items = new Dictionary(); + private readonly IObjectWriter writer = A.Fake(); + private readonly HttpRequest request = A.Fake(); + private ActionContextLogAppender sut; + private ActionContext actionContext; + + [Fact] + public void Append_should_get_requestId() + { + items.Add(nameof(requestId), requestId); + SetupTest(); + + A.CallTo(() => writer.WriteObject(It.IsAny(), It.IsAny>())).Returns(writer); + sut.Append(writer); + + Assert.NotNull(writer); + } + + [Fact] + public void Append_should_put_requestId() + { + SetupTest(); + + sut.Append(writer); + } + + [Fact] + public void Append_should_return_if_no_actionContext() + { + sut = new ActionContextLogAppender(actionContextAccessor.Object); + + sut.Append(writer); + } + + [Fact] + public void Append_should_return_if_no_httpContext_method() + { + A.CallTo(() => request.Method).Returns(string.Empty); + httpContextMock.Setup(x => x.Request).Returns(request); + actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + sut = new ActionContextLogAppender(actionContextAccessor.Object); + + sut.Append(writer); + } + + private void SetupTest() + { + A.CallTo(() => request.Method).Returns("Get"); + httpContextMock.Setup(x => x.Items).Returns(items); + httpContextMock.Setup(x => x.Request).Returns(request); + actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + sut = new ActionContextLogAppender(actionContextAccessor.Object); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/ApiAuthorizeAttributeTests.cs b/tests/Squidex.Tests/Pipeline/ApiAuthorizeAttributeTests.cs new file mode 100644 index 000000000..c37d6c309 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/ApiAuthorizeAttributeTests.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using IdentityServer4.AccessTokenValidation; +using Squidex.Pipeline; +using Squidex.Shared.Identity; +using Xunit; + +namespace Squidex.Tests.Pipeline +{ + public class ApiAuthorizeAttributeTests + { + private ApiAuthorizeAttribute sut = new ApiAuthorizeAttribute(); + + [Fact] + public void AuthenticationSchemes_should_be_default() + { + Assert.Equal(IdentityServerAuthenticationDefaults.AuthenticationScheme, sut.AuthenticationSchemes); + } + + [Fact] + public void MustBeAdmin_Test() + { + sut = new MustBeAdministratorAttribute(); + Assert.Equal(SquidexRoles.Administrator, sut.Roles); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/ApiCostTests.cs b/tests/Squidex.Tests/Pipeline/ApiCostTests.cs new file mode 100644 index 000000000..ec6406e62 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/ApiCostTests.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Pipeline; +using Xunit; +using static Squidex.Pipeline.AppApiFilter; + +namespace Squidex.Tests.Pipeline +{ + public class ApiCostTests + { + private readonly Mock actionContextAccessor = new Mock(); + private readonly RouteData routeData = new RouteData(); + private readonly Mock actionDescriptor = new Mock(); + private readonly IAppPlansProvider appPlanProvider = A.Fake(); + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly long usage = 1; + private readonly Mock httpContextMock = new Mock(); + private readonly IFeatureCollection features = new FeatureCollection(); + private readonly IAppEntity appEntity = A.Fake(); + private readonly IAppFeature appFeature = A.Fake(); + private readonly IAppLimitsPlan appPlan = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private ActionExecutingContext context; + private ActionExecutionDelegate next; + private ApiCostsFilter sut; + + public ApiCostTests() + { + var actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + context = new ActionExecutingContext(actionContext, new List(), new Dictionary(), null); + context.Filters.Add(new ServiceFilterAttribute(typeof(ApiCostsFilter))); + + A.CallTo(() => appEntity.Id).Returns(appId); + A.CallTo(() => appFeature.App).Returns(appEntity); + + features.Set(new AppFeature(appEntity)); + httpContextMock.Setup(x => x.Features).Returns(features); + A.CallTo(() => usageTracker.GetMonthlyCalls(appId.ToString(), DateTime.Today)) + .Returns(usage); + } + + [Fact] + public async Task Should_return_429_status_code_if_max_calls_over_limit() + { + SetupSystem(2, 1); + + next = new ActionExecutionDelegate(async () => + { + return null; + }); + await sut.OnActionExecutionAsync(context, next); + + Assert.Equal(new StatusCodeResult(429).StatusCode, (context.Result as StatusCodeResult).StatusCode); + } + + [Fact] + public async Task Should_call_next_if_weight_is_0() + { + SetupSystem(0, 1); + + var result = 0; + next = new ActionExecutionDelegate(async () => + { + result = 1; + return null; + }); + await sut.OnActionExecutionAsync(context, next); + + Assert.Equal(1, result); + } + + private ApiCostsFilter SetupSystem(double weight, long maxCalls) + { + A.CallTo(() => appPlan.MaxApiCalls).Returns(maxCalls); + A.CallTo(() => appPlanProvider.GetPlanForApp(appFeature.App)).Returns(appPlan); + + sut = new ApiCostsFilter(appPlanProvider, usageTracker); + sut.FilterDefinition = new ApiCostsAttribute(weight); + + return sut; + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/ApiExceptionFilterAttributeTests.cs b/tests/Squidex.Tests/Pipeline/ApiExceptionFilterAttributeTests.cs new file mode 100644 index 000000000..dcb343714 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/ApiExceptionFilterAttributeTests.cs @@ -0,0 +1,101 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Pipeline; +using Xunit; + +namespace Squidex.Tests.Pipeline +{ + public class ApiExceptionFilterAttributeTests + { + private readonly Mock httpContextMock = new Mock(); + private readonly Mock actionDescriptor = new Mock(); + private readonly RouteData routeData = new RouteData(); + private readonly ApiExceptionFilterAttribute sut = new ApiExceptionFilterAttribute(); + private readonly ExceptionContext context; + private ActionContext actionContext; + + public ApiExceptionFilterAttributeTests() + { + actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + context = new ExceptionContext(actionContext, new List()); + } + + [Fact] + public void Domain_Object_Not_Found_Exception_should_be_caught() + { + context.Exception = new DomainObjectNotFoundException("id", typeof(IAppEntity)); + + sut.OnException(context); + + Assert.Equal(new NotFoundResult().StatusCode, (context.Result as NotFoundResult).StatusCode); + } + + [Fact] + public void Domain_Object_Version_Exception_should_be_caught() + { + context.Exception = new DomainObjectVersionException("id", typeof(IAppEntity), 0, 1); + + sut.OnException(context); + var exptectedResult = BuildErrorResult(412, new ErrorDto { Message = context.Exception.Message }); + + Assert.Equal(exptectedResult.StatusCode, (context.Result as ObjectResult).StatusCode); + Assert.StartsWith("Requested version", ((context.Result as ObjectResult).Value as ErrorDto).Message); + } + + [Fact] + public void Domain_Exception_should_be_caught() + { + context.Exception = new DomainException("Domain exception caught."); + + sut.OnException(context); + var exptectedResult = BuildErrorResult(400, new ErrorDto { Message = context.Exception.Message }); + + Assert.Equal(exptectedResult.StatusCode, (context.Result as ObjectResult).StatusCode); + Assert.Equal("Domain exception caught.", ((context.Result as ObjectResult).Value as ErrorDto).Message); + } + + [Fact] + public void Domain_Forbidden_Exception_should_be_caught() + { + context.Exception = new DomainForbiddenException("Domain forbidden exception caught."); + + sut.OnException(context); + var exptectedResult = BuildErrorResult(403, new ErrorDto { Message = context.Exception.Message }); + + Assert.Equal(exptectedResult.StatusCode, (context.Result as ObjectResult).StatusCode); + Assert.Equal("Domain forbidden exception caught.", ((context.Result as ObjectResult).Value as ErrorDto).Message); + } + + [Fact] + public void Validation_Exception_should_be_caught() + { + var errors = new ValidationError("Validation error 1", new string[] { "prop1" }); + context.Exception = new ValidationException("Validation exception caught.", errors); + + sut.OnException(context); + var exptectedResult = BuildErrorResult(400, new ErrorDto { Message = context.Exception.Message }); + + Assert.Equal(exptectedResult.StatusCode, (context.Result as ObjectResult).StatusCode); + Assert.Equal("Validation exception caught: Validation error 1.", ((context.Result as ObjectResult).Value as ErrorDto).Message); + } + + private ObjectResult BuildErrorResult(int code, ErrorDto error) + { + return new ObjectResult(error) { StatusCode = code }; + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/AppApiTests.cs b/tests/Squidex.Tests/Pipeline/AppApiTests.cs new file mode 100644 index 000000000..ffee6ea04 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/AppApiTests.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Pipeline; +using Xunit; + +namespace Squidex.Tests.Pipeline +{ + public class AppApiTests + { + private readonly Mock actionContextAccessor = new Mock(); + private readonly RouteData routeData = new RouteData(); + private readonly Mock actionDescriptor = new Mock(); + private readonly IAppPlansProvider appPlanProvider = A.Fake(); + private readonly IUsageTracker usageTracker = A.Fake(); + private readonly long usage = 1; + private readonly Mock httpContextMock = new Mock(); + private readonly IFeatureCollection features = new FeatureCollection(); + private readonly IAppEntity appEntity = A.Fake(); + private readonly IAppFeature appFeature = A.Fake(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly ActionExecutingContext context; + private readonly AppApiFilter sut; + private ActionExecutionDelegate next; + + public AppApiTests() + { + var actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + context = new ActionExecutingContext(actionContext, new List(), new Dictionary(), null); + context.Filters.Add(new AppApiAttribute()); + context.RouteData.Values.Add("app", "appName"); + + httpContextMock.Setup(x => x.Features).Returns(features); + A.CallTo(() => appProvider.GetAppAsync("appName")).Returns(appEntity); + + sut = new AppApiFilter(appProvider); + } + + [Fact] + public async Task Should_set_features_if_app_found() + { + next = new ActionExecutionDelegate(async () => + { + return null; + }); + await sut.OnActionExecutionAsync(context, next); + + Assert.NotEmpty(context.HttpContext.Features); + } + + [Fact] + public async Task Should_return_not_found_result_if_app_not_found() + { + next = new ActionExecutionDelegate(async () => + { + return null; + }); + + A.CallTo(() => appProvider.GetAppAsync("appName")).Returns((IAppEntity)null); + await sut.OnActionExecutionAsync(context, next); + + var result = context.Result as NotFoundResult; + Assert.NotNull(result); + Assert.Equal((int)HttpStatusCode.NotFound, result.StatusCode); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/AppPermissionAttributeTests.cs b/tests/Squidex.Tests/Pipeline/AppPermissionAttributeTests.cs new file mode 100644 index 000000000..d62ffae95 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/AppPermissionAttributeTests.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Net; +using System.Security.Claims; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure.Security; +using Squidex.Pipeline; +using Xunit; + +namespace Squidex.Tests.Pipeline +{ + public class AppPermissionAttributeTests + { + private readonly Mock httpContextMock = new Mock(); + private readonly Mock mockUser = new Mock(); + private readonly Mock actionDescriptor = new Mock(); + private readonly Mock actionContextAccessor = new Mock(); + private readonly IAppEntity appEntity = A.Fake(); + private readonly ClaimsIdentity identity = new ClaimsIdentity(); + private readonly AppClient client = new AppClient("clientId", "secret", AppClientPermission.Reader); + private readonly IAppFeature appFeature = A.Fake(); + private readonly IFeatureCollection features = new FeatureCollection(); + private readonly RouteData routeData = new RouteData(); + private readonly ActionExecutingContext context; + private ActionContext actionContext; + private Claim clientClaim; + private Claim subjectClaim; + private AppPermissionAttribute sut = new MustBeAppReaderAttribute(); + + public AppPermissionAttributeTests() + { + actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + clientClaim = new Claim("client_id", $"test:clientId"); + subjectClaim = new Claim("sub", "user"); + + var clients = ImmutableDictionary.CreateBuilder(); + clients.Add("clientId", client); + var contributors = ImmutableDictionary.CreateBuilder(); + contributors.Add("user", AppContributorPermission.Owner); + + A.CallTo(() => appFeature.App).Returns(appEntity); + A.CallTo(() => appEntity.Clients).Returns(new AppClients(clients.ToImmutable())); + A.CallTo(() => appEntity.Contributors).Returns(new AppContributors(contributors.ToImmutable())); + features.Set(appFeature); + mockUser.Setup(x => x.Identities).Returns(new List { identity }); + + httpContextMock.Setup(x => x.Features).Returns(features); + httpContextMock.Setup(x => x.User).Returns(mockUser.Object); + + context = new ActionExecutingContext(actionContext, new List(), new Dictionary(), null); + context.Filters.Clear(); + sut = new MustBeAppDeveloperAttribute(); + } + + [Fact] + public void Null_Permission_Returns_Not_Found() + { + // Arrange + sut = new MustBeAppReaderAttribute(); + context.Filters.Add(sut); + mockUser.Setup(x => x.FindFirst(OpenIdClaims.Subject)).Returns((Claim)null); + clientClaim = new Claim("client_id", "test"); + mockUser.Setup(x => x.FindFirst(OpenIdClaims.ClientId)).Returns(clientClaim); + + // Act + sut.OnActionExecuting(context); + + // Assert + var result = context.Result as NotFoundResult; + Assert.NotNull(result); + Assert.Equal((int)HttpStatusCode.NotFound, result.StatusCode); + } + + [Fact] + public void Lower_Permission_Returns_Forbidden() + { + // Arrange + sut = new MustBeAppEditorAttribute(); + context.Filters.Add(sut); + mockUser.Setup(x => x.FindFirst(OpenIdClaims.Subject)).Returns((Claim)null); + mockUser.Setup(x => x.FindFirst(OpenIdClaims.ClientId)).Returns(clientClaim); + + // Act + sut.OnActionExecuting(context); + + // Assert + var result = context.Result as StatusCodeResult; + Assert.NotNull(result); + Assert.Equal((int)HttpStatusCode.Forbidden, result.StatusCode); + } + + [Fact] + public void Higher_Permission_Should_Get_All_Lesser_Permissions() + { + // Arrange + sut = new MustBeAppOwnerAttribute(); + context.Filters.Add(sut); + mockUser.Setup(x => x.FindFirst(OpenIdClaims.Subject)).Returns(subjectClaim); + + // Act + sut.OnActionExecuting(context); + + // Assert + var result = context.HttpContext.User.Identities.First()?.Claims; + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Equal(Enum.GetNames(typeof(AppPermission)).Length, result.Count()); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs new file mode 100644 index 000000000..dc57d19b8 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Squidex.Domain.Apps.Entities.Assets.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Pipeline.CommandMiddlewares; +using Xunit; + +namespace Squidex.Tests.Pipeline.CommandMiddlewares +{ + public class ETagCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly IHeaderDictionary headers = new HeaderDictionary { { "If-Match", "1" } }; + private readonly UpdateAsset command = new UpdateAsset(); + private readonly EntitySavedResult entitySavedResult = new EntitySavedResult(1); + private readonly ETagCommandMiddleware sut; + + public ETagCommandMiddlewareTests() + { + A.CallTo(() => httpContextAccessor.HttpContext.Request.Headers).Returns(headers); + sut = new ETagCommandMiddleware(httpContextAccessor); + } + + [Fact] + public async Task Should_add_etag_header_and_expected_version() + { + var context = new CommandContext(command, commandBus); + context.Complete(entitySavedResult); + + await sut.HandleAsync(context); + + Assert.Equal(1, context.Command.ExpectedVersion); + Assert.Equal(new StringValues("1"), httpContextAccessor.HttpContext.Response.Headers["ETag"]); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs new file mode 100644 index 000000000..78e2e45c9 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Security; +using Squidex.Pipeline.CommandMiddlewares; +using Xunit; + +namespace Squidex.Tests.Pipeline.CommandMiddlewares +{ + public class EnrichWithActorCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly CreateContent command = new CreateContent { Actor = null }; + + [Fact] + public async Task HandleAsync_should_throw_security_exception() + { + var context = new CommandContext(command, commandBus); + var sut = SetupSystem(null, out string claimValue); + + await Assert.ThrowsAsync(() => + { + return sut.HandleAsync(context); + }); + } + + [Fact] + public async Task HandleAsync_should_find_actor_from_subject() + { + var context = new CommandContext(command, commandBus); + var sut = SetupSystem("subject", out string claimValue); + + await sut.HandleAsync(context); + + Assert.Equal(claimValue, command.Actor.Identifier); + } + + [Fact] + public async Task HandleAsync_should_find_actor_from_client() + { + var context = new CommandContext(command, commandBus); + var sut = SetupSystem("client", out string claimValue); + + await sut.HandleAsync(context); + + Assert.Equal(claimValue, command.Actor.Identifier); + } + + private EnrichWithActorCommandMiddleware SetupSystem(string refTokenType, out string claimValue) + { + Claim actorClaim; + claimValue = Guid.NewGuid().ToString(); + var user = new ClaimsPrincipal(); + var claimsIdentity = new ClaimsIdentity(); + switch (refTokenType) + { + case "subject": + actorClaim = new Claim(OpenIdClaims.Subject, claimValue); + claimsIdentity.AddClaim(actorClaim); + break; + case "client": + actorClaim = new Claim(OpenIdClaims.ClientId, claimValue); + claimsIdentity.AddClaim(actorClaim); + break; + } + + user.AddIdentity(claimsIdentity); + A.CallTo(() => httpContextAccessor.HttpContext.User).Returns(user); + return new EnrichWithActorCommandMiddleware(httpContextAccessor); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..b4df7d6ad --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -0,0 +1,65 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +using System; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.State; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Pipeline; +using Squidex.Pipeline.CommandMiddlewares; +using Xunit; +using static Squidex.Pipeline.AppApiFilter; + +namespace Squidex.Tests.Pipeline.CommandMiddlewares +{ + public class EnrichWithAppIdCommandMiddlewareTests + { + private readonly IHttpContextAccessor httpContextAccessor = A.Fake(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly CreateContent command = new CreateContent { AppId = null }; + + [Fact] + public async Task HandleAsync_should_throw_exception_if_app_id_not_found() + { + var context = new CommandContext(command, commandBus); + var sut = SetupSystem(null); + + await Assert.ThrowsAsync(() => + { + return sut.HandleAsync(context); + }); + } + + [Fact] + public async Task HandleAsync_should_find_app_id_from_features() + { + var context = new CommandContext(command, commandBus); + var app = new AppState + { + Name = "app", + Id = Guid.NewGuid() + }; + var sut = SetupSystem(app); + + await sut.HandleAsync(context); + + Assert.Equal(new NamedId(app.Id, app.Name), command.AppId); + } + + private EnrichWithAppIdCommandMiddleware SetupSystem(IAppEntity app) + { + var appFeature = app == null ? null : new AppFeature(app); + A.CallTo(() => httpContextAccessor.HttpContext.Features.Get()).Returns(appFeature); + + return new EnrichWithAppIdCommandMiddleware(httpContextAccessor); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..c852683a3 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -0,0 +1,124 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Moq; +using Squidex.Domain.Apps.Core; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.State; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Pipeline.CommandMiddlewares; +using Xunit; + +namespace Squidex.Tests.Pipeline.CommandMiddlewares +{ + public class EnrichWithSchemaIdCommandMiddlewareTests + { + private readonly Mock actionContextAccessor = new Mock(); + private readonly ICommandBus commandBus = A.Fake(); + private readonly Mock httpContextMock = new Mock(); + private readonly Mock actionDescriptor = new Mock(); + private readonly IAppProvider appProvider = A.Fake(); + private readonly Guid appId = Guid.NewGuid(); + private readonly string appName = "app"; + private readonly Guid schemaId = Guid.NewGuid(); + private readonly string schemaName = "schema"; + private readonly CreateContent command = new CreateContent(); + private readonly RouteData routeData = new RouteData(); + private ISchemaEntity schema; + + [Fact] + public async Task HandleAsync_should_throw_exception_if_schema_not_found() + { + var context = new CommandContext(command, commandBus); + var sut = SetupSchemaCommand(false); + + await Assert.ThrowsAsync(() => + { + return sut.HandleAsync(context); + }); + } + + [Fact] + public async Task HandleAsync_should_find_schema_id_by_name() + { + var context = new CommandContext(command, commandBus); + SetupSchema(); + + var sut = SetupSchemaCommand(false); + + await sut.HandleAsync(context); + + Assert.Equal(new NamedId(schemaId, schemaName), command.SchemaId); + } + + [Fact] + public async Task HandleAsync_should_find_schema_id_by_id() + { + var context = new CommandContext(command, commandBus); + SetupSchema(); + + var sut = SetupSchemaCommand(true); + + await sut.HandleAsync(context); + + Assert.Equal(new NamedId(schemaId, schemaName), command.SchemaId); + } + + private void SetupSchema() + { + var schemaDef = new Schema(schemaName); + var stringValidatorProperties = new StringFieldProperties + { + Pattern = "A-Z" + }; + var stringFieldWithValidator = new StringField(1, "validator", Partitioning.Invariant, stringValidatorProperties); + + schemaDef = schemaDef.AddField(stringFieldWithValidator); + + schema = new SchemaState + { + Name = schemaName, + Id = schemaId, + AppId = new NamedId(appId, appName), + SchemaDef = schemaDef + }; + } + + private EnrichWithSchemaIdCommandMiddleware SetupSchemaCommand(bool byId) + { + command.AppId = new NamedId(appId, appName); + + if (byId) + { + routeData.Values.Add("name", schemaId.ToString()); + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)).Returns(schema); + } + else + { + routeData.Values.Add("name", "schema"); + A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaName)).Returns(schema); + } + + var actionContext = new ActionContext(httpContextMock.Object, routeData, actionDescriptor.Object); + actionContextAccessor.Setup(x => x.ActionContext).Returns(actionContext); + return new EnrichWithSchemaIdCommandMiddleware(appProvider, actionContextAccessor.Object); + } + } +} diff --git a/tests/Squidex.Tests/Pipeline/Swagger/SwaggerHelperTests.cs b/tests/Squidex.Tests/Pipeline/Swagger/SwaggerHelperTests.cs new file mode 100644 index 000000000..413484034 --- /dev/null +++ b/tests/Squidex.Tests/Pipeline/Swagger/SwaggerHelperTests.cs @@ -0,0 +1,120 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using NJsonSchema; +using NSwag; +using NSwag.AspNetCore; +using NSwag.SwaggerGeneration; +using Squidex.Config; +using Squidex.Infrastructure; +using Squidex.Pipeline.Swagger; +using Xunit; + +namespace Squidex.Tests.Pipeline.Swagger +{ + public class SwaggerHelperTests + { + private readonly IHttpContextAccessor contextAccessor = A.Fake(); + private readonly string appName = "app"; + private readonly string host = "kraken"; + private readonly MyUrlsOptions myUrlsOptions = new MyUrlsOptions { BaseUrl = "www.test.com" }; + private readonly SwaggerOperation operation = new SwaggerOperation(); + + [Fact] + public void Should_load_docs() + { + var doc = SwaggerHelper.LoadDocs("security"); + Assert.StartsWith("Squidex", doc); + } + + [Fact] + public void Should_throw_exception_when_base_url_is_empty() + { + var testUrlOptions = new MyUrlsOptions(); + Assert.Throws(() => testUrlOptions.BuildUrl("/api")); + } + + [Fact] + public void Should_create_swagger_document() + { + var swaggerDoc = CreateSwaggerDocument(); + + Assert.NotNull(swaggerDoc.Tags); + Assert.Contains("application/json", swaggerDoc.Consumes); + Assert.Contains("application/json", swaggerDoc.Produces); + Assert.NotNull(swaggerDoc.Info.ExtensionData["x-logo"]); + Assert.Equal($"Squidex API for {appName} App", swaggerDoc.Info.Title); + Assert.Equal("/api", swaggerDoc.BasePath); + Assert.Equal(host, swaggerDoc.Host); + Assert.NotEmpty(swaggerDoc.SecurityDefinitions); + } + + [Fact] + public void Should_create_OAuth_schema() + { + var oauthSchema = SwaggerHelper.CreateOAuthSchema(myUrlsOptions); + + Assert.Equal(myUrlsOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token"), oauthSchema.TokenUrl); + Assert.Equal(SwaggerSecuritySchemeType.OAuth2, oauthSchema.Type); + Assert.Equal(SwaggerOAuth2Flow.Application, oauthSchema.Flow); + Assert.NotEmpty(oauthSchema.Scopes); + Assert.Contains(myUrlsOptions.BuildUrl($"{Constants.IdentityServerPrefix}/connect/token"), + oauthSchema.Description); + Assert.DoesNotContain("", oauthSchema.Description); + } + + [Fact] + public async Task Should_get_error_dto_schema() + { + var swaggerDoc = CreateSwaggerDocument(); + + var schemaGenerator = new SwaggerJsonSchemaGenerator(new SwaggerSettings()); + var schemaResolver = new SwaggerSchemaResolver(swaggerDoc, new SwaggerSettings()); + var errorDto = await schemaGenerator.GetErrorDtoSchemaAsync(schemaResolver); + + Assert.NotNull(errorDto); + } + + [Fact] + public void Should_add_query_parameter() + { + operation.AddQueryParameter("test", JsonObjectType.String, "Test parameter"); + Assert.Contains(operation.Parameters, p => p.Kind == SwaggerParameterKind.Query); + } + + [Fact] + public void Should_add_path_parameter() + { + operation.AddPathParameter("test", JsonObjectType.String, "Test parameter"); + Assert.Contains(operation.Parameters, p => p.Kind == SwaggerParameterKind.Path); + } + + [Fact] + public void Should_add_body_parameter() + { + operation.AddBodyParameter("test", null, "Test parameter"); + Assert.Contains(operation.Parameters, p => p.Kind == SwaggerParameterKind.Body); + } + + [Fact] + public void Should_add_response_parameter() + { + operation.AddResponse("200", "Test is ok"); + Assert.Contains(operation.Responses, r => r.Key == "200"); + } + + private SwaggerDocument CreateSwaggerDocument() + { + A.CallTo(() => contextAccessor.HttpContext.Request.Scheme).Returns("http"); + A.CallTo(() => contextAccessor.HttpContext.Request.Host).Returns(new HostString(host)); + return SwaggerHelper.CreateApiDocument(contextAccessor.HttpContext, myUrlsOptions, appName); + } + } +} diff --git a/tests/Squidex.Tests/Squidex.Tests.csproj b/tests/Squidex.Tests/Squidex.Tests.csproj new file mode 100644 index 000000000..08d8df293 --- /dev/null +++ b/tests/Squidex.Tests/Squidex.Tests.csproj @@ -0,0 +1,39 @@ + + + + netcoreapp2.0 + + + + + + + + + + + + + + + + + + + + + ..\..\..\identityserver\CivicPlusIdentityServer.SDK.NetCore\bin\Debug\netcoreapp2.0\CivicPlusIdentityServer.SDK.NetCore.dll + + + ..\..\..\restsharp_core\RestSharp\bin\Debug\netstandard2.0\RestSharp.dll + + + ..\..\..\..\Desktop\RestSharp.NetCore.dll + + + + + + + +