-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
OSOE-638: Feature to edit shell settings from the admin in Lombiq.Hosting.Tenants
- Loading branch information
Showing
22 changed files
with
579 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
Lombiq.Hosting.Tenants.Management.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
using Lombiq.Tests.UI.Extensions; | ||
using Lombiq.Tests.UI.Services; | ||
using Newtonsoft.Json.Linq; | ||
using OpenQA.Selenium; | ||
using Shouldly; | ||
using System.Threading.Tasks; | ||
|
||
namespace Lombiq.Hosting.Tenants.Management.Tests.UI.Extensions; | ||
|
||
public static class TestCaseUITestContextExtensions | ||
{ | ||
public static async Task TestShellSettingsEditorFeatureAsync(this UITestContext context) | ||
{ | ||
await context.SignInDirectlyAsync(); | ||
await context.GoToAdminRelativeUrlAsync("/Tenants/Edit/Default"); | ||
|
||
// Expected JSON string. | ||
#pragma warning disable JSON002 // Probable JSON string detected | ||
await context.FillInEditorThenCheckValueAsync( | ||
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{\"FirstTestKey\": \"FirstTestValue\",\"SecondTestKey\": \"SecondTestValue\"}}}}", | ||
"FirstTestKey", | ||
"FirstTestValue"); | ||
|
||
await context.FillInEditorThenCheckValueAsync( | ||
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{\"NewKey\": \"NewValue\",\"SecondTestKey\": \"SecondTestValue\"}}}}", | ||
"NewKey", | ||
"NewValue"); | ||
|
||
await context.FillInEditorThenCheckValueAsync( | ||
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{\"SecondTestKey\": \"SecondTestValue\"}}}}", | ||
"NewKey", | ||
string.Empty); | ||
|
||
CheckEditorValue(context, "SecondTestKey", "SecondTestValue"); | ||
|
||
await context.FillInEditorThenCheckValueAsync( | ||
"{\"TestKey\":{\"TestSubKey\":{\"TestSubOptions\":{}}}}", | ||
"SecondTestKey", | ||
string.Empty); | ||
#pragma warning restore JSON002 // Probable JSON string detected | ||
|
||
await context.FillInEditorThenCheckValueAsync( | ||
string.Empty, | ||
"SecondTestKey", | ||
string.Empty); | ||
} | ||
|
||
private static async Task FillInEditorThenCheckValueAsync(this UITestContext context, string text, string keyToCheck, string expectedValue) | ||
{ | ||
context.FillInMonacoEditor("Json_editor", text); | ||
await context.ClickReliablyOnAsync(By.XPath("//button[contains(.,'Save settings')]")); | ||
CheckEditorValue(context, keyToCheck, expectedValue); | ||
} | ||
|
||
private static void CheckEditorValue(this UITestContext context, string keyToCheck, string expectedValue) | ||
{ | ||
var editorText = context.GetMonacoEditorText("Json_editor"); | ||
var editorJson = string.IsNullOrEmpty(editorText) ? "{}" : editorText; | ||
|
||
var editorValue = JObject.Parse(editorJson); | ||
editorValue.SelectToken($"TestKey.TestSubKey.TestSubOptions.{keyToCheck}")?.ToString().ShouldBeAsString(expectedValue); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
Copyright © 2021, [Lombiq Technologies Ltd.](https://lombiq.com) | ||
|
||
All rights reserved. | ||
|
||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
|
||
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
|
||
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
|
||
- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
33 changes: 33 additions & 0 deletions
33
Lombiq.Hosting.Tenants.Management.Tests.UI/Lombiq.Hosting.Tenants.Management.Tests.UI.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<Title>Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions</Title> | ||
<Authors>Lombiq Technologies</Authors> | ||
<Copyright>Copyright © 2021, Lombiq Technologies Ltd.</Copyright> | ||
<Description>Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions: Extension methods that test tenants management for Orchard Core.</Description> | ||
<PackageIcon>NuGetIcon.png</PackageIcon> | ||
<PackageTags>OrchardCore;Lombiq;AspNetCore;Multitenancy;SaaS</PackageTags> | ||
<RepositoryUrl>https://github.com/Lombiq/Hosting-Tenants</RepositoryUrl> | ||
<PackageProjectUrl>https://github.com/Lombiq/Hosting-Tenants/blob/dev/Lombiq.Hosting.Tenants.Management.Tests.UI/Readme.md</PackageProjectUrl> | ||
<PackageLicenseFile>License.md</PackageLicenseFile> | ||
</PropertyGroup> | ||
|
||
<ItemGroup Condition="'$(NuGetBuild)' != 'true'"> | ||
<ProjectReference Include="..\..\..\..\test\Lombiq.UITestingToolbox\Lombiq.Tests.UI\Lombiq.Tests.UI.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup Condition="'$(NuGetBuild)' == 'true'"> | ||
<PackageReference Include="Lombiq.Tests.UI" Version="8.1.0-alpha.1.osoe-638" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Include="License.md" Pack="true" PackagePath="" /> | ||
<None Include="NuGetIcon.png" Pack="true" PackagePath="" /> | ||
<None Include="Readme.md" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Lombiq Hosting - Tenants Management for Orchard Core - UI Test Extensions | ||
|
||
## About | ||
|
||
Extension methods that test tenants management for Orchard Core, with the help of [Lombiq UI Testing Toolbox for Orchard Core](https://github.com/Lombiq/UI-Testing-Toolbox). | ||
|
||
Call these from a UI test project to verify the module's basic features; as seen in [Open-Source Orchard Core Extensions](https://github.com/Lombiq/Open-Source-Orchard-Core-Extensions). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
Lombiq.Hosting.Tenants.Management/Controllers/ShellSettingsEditorController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
using Lombiq.Hosting.Tenants.Management.Constants; | ||
using Lombiq.Hosting.Tenants.Management.Models; | ||
using Lombiq.Hosting.Tenants.Management.Service; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
using Microsoft.Extensions.Configuration; | ||
using OrchardCore.DisplayManagement.Notify; | ||
using OrchardCore.Environment.Shell; | ||
using OrchardCore.Environment.Shell.Configuration; | ||
using OrchardCore.Locking.Distributed; | ||
using OrchardCore.Modules; | ||
using OrchardCore.Mvc.Core.Utilities; | ||
using OrchardCore.Tenants.Controllers; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text.Json; | ||
using System.Threading.Tasks; | ||
using static OrchardCore.Tenants.Permissions; | ||
|
||
namespace Lombiq.Hosting.Tenants.Management.Controllers; | ||
|
||
[Feature(FeatureNames.ShellSettingsEditor)] | ||
public class ShellSettingsEditorController : Controller | ||
{ | ||
private readonly IAuthorizationService _authorizationService; | ||
private readonly IShellHost _shellHost; | ||
private readonly IShellConfigurationSources _shellConfigurationSources; | ||
private readonly IDistributedLock _distributedLock; | ||
private readonly INotifier _notifier; | ||
private readonly IHtmlLocalizer<ShellSettingsEditorController> H; | ||
|
||
public ShellSettingsEditorController( | ||
IAuthorizationService authorizationService, | ||
IShellHost shellHost, | ||
IShellConfigurationSources shellConfigurationSources, | ||
IDistributedLock distributedLock, | ||
INotifier notifier, | ||
IHtmlLocalizer<ShellSettingsEditorController> htmlLocalizer) | ||
{ | ||
_authorizationService = authorizationService; | ||
_shellHost = shellHost; | ||
_shellConfigurationSources = shellConfigurationSources; | ||
_distributedLock = distributedLock; | ||
_notifier = notifier; | ||
H = htmlLocalizer; | ||
} | ||
|
||
[HttpPost] | ||
[ValidateAntiForgeryToken] | ||
public async Task<IActionResult> Edit(ShellSettingsEditorViewModel model) | ||
{ | ||
if (!await _authorizationService.AuthorizeAsync(User, ManageTenants) || | ||
!_shellHost.TryGetSettings(model.TenantId, out var shellSettings)) | ||
{ | ||
return NotFound(); | ||
} | ||
|
||
model.Json ??= "{}"; | ||
if (!IsValidJson(model.Json)) | ||
{ | ||
await _notifier.ErrorAsync(H["Please provide valid JSON input for shell settings."]); | ||
TempData["ValidationErrorJson"] = model.Json; | ||
|
||
return RedirectToAction( | ||
nameof(AdminController.Edit), | ||
typeof(AdminController).ControllerName(), | ||
new | ||
{ | ||
area = "OrchardCore.Tenants", | ||
id = model.TenantId, | ||
}); | ||
} | ||
|
||
var tenantConfiguration = new JsonConfigurationParser().ParseConfiguration(model.Json); | ||
var newTenantConfiguration = new Dictionary<string, string>(); | ||
|
||
var tenantSettingsPrefix = $"{model.TenantId}Prefix:"; | ||
var currentSettings = shellSettings.ShellConfiguration.AsEnumerable() | ||
.Where(item => item.Value != null && | ||
item.Key.Contains(tenantSettingsPrefix)) | ||
.ToDictionary(key => key.Key.Replace(tenantSettingsPrefix, string.Empty), value => value.Value); | ||
|
||
foreach (var key in tenantConfiguration.Keys) | ||
{ | ||
var tenantSettingsPrefixWithKey = $"{tenantSettingsPrefix}{key}"; | ||
if (shellSettings[key] != tenantConfiguration[key]) | ||
{ | ||
newTenantConfiguration[tenantSettingsPrefixWithKey] = tenantConfiguration[key]; | ||
newTenantConfiguration[key] = tenantConfiguration[key]; | ||
} | ||
} | ||
|
||
var deletableKeys = currentSettings | ||
.Where(item => !tenantConfiguration.ContainsKey(item.Key)) | ||
.Select(item => item.Key); | ||
|
||
foreach (var key in deletableKeys) | ||
{ | ||
var tenantSettingsPrefixWithKey = $"{tenantSettingsPrefix}{key}"; | ||
newTenantConfiguration[key] = null; | ||
newTenantConfiguration[tenantSettingsPrefixWithKey] = null; | ||
} | ||
|
||
var (locker, locked) = | ||
await _distributedLock.TryAcquireLockAsync( | ||
"LOMBIQ_HOSTING_TENANTS_MANAGEMENT_SHELL_SETTINGS_EDITOR_LOCK", | ||
TimeSpan.FromSeconds(10)); | ||
|
||
if (!locked) | ||
{ | ||
throw new TimeoutException($"Failed to acquire a lock before saving settings to the tenant: {model.TenantId}."); | ||
} | ||
|
||
await using var acquiredLock = locker; | ||
|
||
// We are using the shell configuration sources directly because using IShellHost.UpdateShellSettingsAsync would | ||
// not save settings that has a key with multiple sections, see | ||
// https://github.com/OrchardCMS/OrchardCore/issues/14481. Once this is fixed, we can get rid of the locking and | ||
// retrieve and save the shell settings settings with IShellHost. | ||
await _shellConfigurationSources.SaveAsync(shellSettings.Name, newTenantConfiguration); | ||
await _shellHost.UpdateShellSettingsAsync(shellSettings); | ||
|
||
return RedirectToAction( | ||
nameof(AdminController.Edit), | ||
typeof(AdminController).ControllerName(), | ||
new | ||
{ | ||
area = "OrchardCore.Tenants", | ||
id = model.TenantId, | ||
}); | ||
} | ||
|
||
private static bool IsValidJson(string json) | ||
{ | ||
try | ||
{ | ||
JsonDocument.Parse(json); | ||
return true; | ||
} | ||
catch (JsonException) | ||
{ | ||
return false; | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
Lombiq.Hosting.Tenants.Management/Filters/ShellSettingsEditorFilter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
using Lombiq.Hosting.Tenants.Management.Models; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.Filters; | ||
using OrchardCore.DisplayManagement; | ||
using OrchardCore.DisplayManagement.Layout; | ||
using OrchardCore.Environment.Shell; | ||
using OrchardCore.Environment.Shell.Configuration; | ||
using OrchardCore.Mvc.Core.Utilities; | ||
using OrchardCore.Tenants.Controllers; | ||
using System.Threading.Tasks; | ||
|
||
namespace Lombiq.Hosting.Tenants.Management.Filters; | ||
|
||
public class ShellSettingsEditorFilter : IAsyncResultFilter | ||
{ | ||
private readonly ILayoutAccessor _layoutAccessor; | ||
private readonly IShapeFactory _shapeFactory; | ||
private readonly IShellHost _shellHost; | ||
|
||
public ShellSettingsEditorFilter( | ||
ILayoutAccessor layoutAccessor, | ||
IShapeFactory shapeFactory, | ||
IShellHost shellHost) | ||
{ | ||
_layoutAccessor = layoutAccessor; | ||
_shapeFactory = shapeFactory; | ||
_shellHost = shellHost; | ||
} | ||
|
||
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) | ||
{ | ||
var actionRouteController = context.ActionDescriptor.RouteValues["Controller"]; | ||
var actionRouteArea = context.ActionDescriptor.RouteValues["Area"]; | ||
var actionRouteValue = context.ActionDescriptor.RouteValues["Action"]; | ||
|
||
if (actionRouteController == typeof(AdminController).ControllerName() && | ||
actionRouteArea == $"{nameof(OrchardCore)}.{nameof(OrchardCore.Tenants)}" && | ||
actionRouteValue is nameof(AdminController.Edit) && | ||
context.Result is ViewResult) | ||
{ | ||
var tenantName = context.RouteData.Values["Id"].ToString(); | ||
if (!_shellHost.TryGetSettings(tenantName, out var shellSettings)) | ||
{ | ||
await next(); | ||
return; | ||
} | ||
|
||
var layout = await _layoutAccessor.GetLayoutAsync(); | ||
var contentZone = layout.Zones["Content"]; | ||
|
||
(context.Controller as Controller) | ||
!.TempData | ||
.TryGetValue( | ||
"ValidationErrorJson", | ||
out var validationErrorJson); | ||
|
||
var editableItems = shellSettings.ShellConfiguration.AsJsonNode(); | ||
var editorJson = string.IsNullOrEmpty(validationErrorJson?.ToString()) | ||
? editableItems[$"{tenantName}Prefix"]?.ToJsonString() | ||
: validationErrorJson.ToString(); | ||
|
||
await contentZone.AddAsync( | ||
await _shapeFactory.CreateAsync<ShellSettingsEditorViewModel>( | ||
"ShellSettingsEditor", | ||
viewModel => | ||
{ | ||
viewModel.Json = editorJson; | ||
viewModel.TenantId = tenantName; | ||
}), | ||
"10"); | ||
} | ||
|
||
await next(); | ||
} | ||
} |
Oops, something went wrong.