Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEST-423: Adding E-mail quota management module #83

Merged
merged 44 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2a4d5f3
Adding email quota management module
wAsnk Sep 13, 2023
8a512a4
Adding storage and settings
wAsnk Sep 13, 2023
7196431
Adding feature name
wAsnk Sep 13, 2023
af34b79
Adding basic logic for quota management
wAsnk Sep 13, 2023
3d93340
Implementing decorator service
wAsnk Sep 13, 2023
107c41f
Adding background task to reset quota every new month
wAsnk Sep 13, 2023
d421cf6
Init services
wAsnk Sep 13, 2023
0a4e0c4
Adding docs
wAsnk Sep 13, 2023
c424981
Only limit if the preset smtp settings is being used.
wAsnk Sep 13, 2023
d398695
Adding dependencies
wAsnk Sep 13, 2023
112785e
Adding admin error filter
wAsnk Sep 13, 2023
5c966ab
Should be equal to
wAsnk Sep 14, 2023
2f682c7
Correcting filter logic
wAsnk Sep 14, 2023
f8d62b8
Upgrade to 1.7.0
wAsnk Sep 14, 2023
59be9c2
Adding migration
wAsnk Sep 14, 2023
822bdf4
Adding filter
wAsnk Sep 14, 2023
59cae2a
Adding new function
wAsnk Sep 14, 2023
47698e0
Using final text
wAsnk Sep 14, 2023
4a10aba
Using service logic everywhere
wAsnk Sep 14, 2023
3b29054
Sending email to siteowner users
wAsnk Sep 14, 2023
0783b84
Implementing email sending logic
wAsnk Sep 15, 2023
65c7f3b
Adding features guard for email quota
wAsnk Sep 15, 2023
d1ba86e
Adding limit check
wAsnk Sep 16, 2023
c4e4b0c
Some modifications
wAsnk Sep 16, 2023
c7f329c
Adding quota filter
wAsnk Sep 16, 2023
787720c
Cleaning up
wAsnk Sep 16, 2023
4b27534
Updating project file
wAsnk Sep 16, 2023
83483f5
Adding docs
wAsnk Sep 16, 2023
a21428e
Updating docs
wAsnk Sep 16, 2023
1643afe
Fixing code analyzer violations
wAsnk Sep 16, 2023
ab50761
Fixing markdown analyzer violations
wAsnk Sep 16, 2023
e6b1dc0
Adding UI test
wAsnk Sep 17, 2023
6018c0b
Merge remote-tracking branch 'origin/issue/NEST-423' into issue/NEST-423
wAsnk Sep 17, 2023
29f1bff
Sending notification email in deferred task
wAsnk Sep 18, 2023
857b6cd
Using paragraph instead of header
wAsnk Sep 18, 2023
d804769
Fixing text
wAsnk Sep 18, 2023
885aeeb
Fixing code analyzer violations
wAsnk Sep 18, 2023
ff23664
Update Lombiq.Hosting.Tenants.EmailQuotaManagement/Readme.md
wAsnk Sep 19, 2023
8fa73a3
Setting back the default
wAsnk Sep 19, 2023
2029161
Renaming variable to better represent its meaning
wAsnk Sep 19, 2023
dd4fb0b
Using generic messages
wAsnk Sep 19, 2023
17be57c
Adding default value
wAsnk Sep 21, 2023
540fa41
Update readme and adding to solution
wAsnk Sep 21, 2023
d7b56a3
Conditional package reference
wAsnk Sep 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Lombiq.Tests.UI.Services;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI.Extensions;

public static class EmailQuotaManagementExtensions
{
public static void SetEmailQuotaManagementOptionsForUITest(
this OrchardCoreUITestExecutorConfiguration configuration,
long maximumEmails,
string emailHost = "localhost")
{
configuration.OrchardCoreConfiguration.BeforeAppStart +=
(_, argumentsBuilder) =>
{
argumentsBuilder
.AddWithValue(
"OrchardCore:Lombiq_Hosting_Tenants_EmailQuotaManagement:EmailQuota",
maximumEmails);

argumentsBuilder
.AddWithValue(
"OrchardCore:SmtpSettings:Host",
emailHost);

return Task.CompletedTask;
};

configuration.UseSmtpService = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.Helpers;
using Lombiq.Tests.UI.Services;
using OpenQA.Selenium;
using System;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI.Extensions;

public static class TestCaseUITestContextExtensions
{
private const string SuccessfulSubject = "Successful test message";
private const string UnSuccessfulSubject = "Unsuccessful test message";
private const string DashboardWarning =
"//p[contains(@class,'alert-danger')][contains(.,'It seems that your site sent out more e-mails')]";

public static async Task TestEmailQuotaManagementBehaviorAsync(
this UITestContext context,
int maximumEmailQuota,
bool moduleShouldInterfere = true)
{
await context.SignInDirectlyAndGoToDashboardAsync();

context.Missing(By.XPath(DashboardWarning));

await context.GoToAdminRelativeUrlAsync("/Settings/email");

CheckEmailsSentWarningMessage(context, exists: moduleShouldInterfere, maximumEmailQuota, 0);

await SendTestEmailAsync(context, SuccessfulSubject);
context.SuccessMessageExists();

CheckEmailsSentWarningMessage(context, exists: moduleShouldInterfere, maximumEmailQuota, 1);

await context.GoToDashboardAsync();
context.CheckExistence(By.XPath(DashboardWarning), exists: moduleShouldInterfere);

await SendTestEmailAsync(context, UnSuccessfulSubject);
await context.GoToSmtpWebUIAsync();
context.CheckExistence(ByHelper.SmtpInboxRow(SuccessfulSubject), exists: true);
context.CheckExistence(
ByHelper.SmtpInboxRow("[Action Required] Your DotNest site has run over its e-mail quota"),
exists: moduleShouldInterfere);
context.CheckExistence(ByHelper.SmtpInboxRow(UnSuccessfulSubject), exists: !moduleShouldInterfere);
}

private static void CheckEmailsSentWarningMessage(UITestContext context, bool exists, int maximumEmailQuota, int currentEmailCount) =>
context.CheckExistence(
By.XPath($"//p[contains(@class,'alert-warning')][contains(.,'{currentEmailCount.ToTechnicalString()} emails" +
$" from the total of {maximumEmailQuota.ToTechnicalString()}.')]"),
exists);

private static async Task SendTestEmailAsync(UITestContext context, string subject)
{
await context.GoToAdminRelativeUrlAsync("/Email/Index");
await context.FillInWithRetriesAsync(By.Id("To"), "recipient@example.com");
await context.FillInWithRetriesAsync(By.Id("Subject"), subject);
await context.FillInWithRetriesAsync(By.Id("Body"), "Hi, this is a test.");

await ReliabilityHelper.DoWithRetriesOrFailAsync(
async () =>
{
try
{
await context.ClickReliablyOnAsync(By.Id("emailtestsend")); // #spell-check-ignore-line
return true;
}
catch (WebDriverException ex) when (ex.Message.Contains("move target out of bounds"))
{
return false;
}
});
}
}
13 changes: 13 additions & 0 deletions Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI/License.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright © 2023, [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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);.git*;node_modules\**</DefaultItemExcludes>
</PropertyGroup>

<PropertyGroup>
<Title>Lombiq Hosting - Tenants Email Quota Management - UI Test Extensions</Title>
<Authors>Lombiq Technologies</Authors>
<Copyright>Copyright © 2023, Lombiq Technologies Ltd.</Copyright>
<Description>Extension methods that test various features in Lombiq Hosting - Tenants Email Quota Management, with the help of Lombiq UI Testing Toolbox for Orchard Core. See the project website for detailed documentation.</Description>
<PackageIcon>NuGetIcon.png</PackageIcon>
<PackageTags>OrchardCore;Lombiq;AspNetCore;Tenants;LombiqHostingSuite;Shouldly;xUnit;UITesting;Testing</PackageTags>
<RepositoryUrl>https://github.com/Lombiq/Hosting-Tenants</RepositoryUrl>
<PackageProjectUrl>https://github.com/Lombiq/Hosting-Tenants/blob/dev/Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI/Readme.md</PackageProjectUrl>
<PackageLicenseFile>License.md</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
<None Include="License.md" Pack="true" PackagePath="" />
<None Include="Readme.md" />
<None Include="NuGetIcon.png" Pack="true" PackagePath="" />
</ItemGroup>

<ItemGroup>
<None Remove="node_modules\**" />
<None Remove="Tests\**" />
</ItemGroup>

<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.0.0" />
</ItemGroup>

</Project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions Lombiq.Hosting.Tenants.EmailQuotaManagement.Tests.UI/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Lombiq Hosting - Tenants Email Quota Management - UI Test Extensions

## About

Extension methods that test various features in Lombiq Hosting - Tenants Email Quota Management, 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).

## Contributing and support

Bug reports, feature requests, comments, questions, code contributions and love letters are warmly welcome. You can send them to us via GitHub issues and pull requests. Please adhere to our [open-source guidelines](https://lombiq.com/open-source-guidelines) while doing so.

This project is developed by [Lombiq Technologies](https://lombiq.com/). Commercial-grade support is available through Lombiq.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Constants;

public static class EmailQuotaOptionsConstants
{
public const int DefaultEmailQuota = 1000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Constants;

public static class FeatureNames
{
private const string Module = "Lombiq.Hosting";

public const string Tenants = Module + "." + nameof(Tenants);

public const string EmailQuotaManagement = Tenants + "." + nameof(EmailQuotaManagement);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Lombiq.Hosting.Tenants.EmailQuotaManagement.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using OrchardCore.Admin.Controllers;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Layout;
using OrchardCore.Mvc.Core.Utilities;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Filters;

public class EmailQuotaErrorFilter : IAsyncResultFilter
{
private readonly IShapeFactory _shapeFactory;
private readonly ILayoutAccessor _layoutAccessor;
private readonly IQuotaService _quotaService;

public EmailQuotaErrorFilter(
IShapeFactory shapeFactory,
ILayoutAccessor layoutAccessor,
IQuotaService quotaService)
{
_shapeFactory = shapeFactory;
_layoutAccessor = layoutAccessor;
_quotaService = quotaService;
}

public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (!context.IsAdmin())
{
await next();
return;
}

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.Admin)}" &&
actionRouteValue is nameof(AdminController.Index) &&
context.Result is ViewResult &&
_quotaService.ShouldLimitEmails() &&
(await _quotaService.IsQuotaOverTheLimitAsync()).IsOverQuota)
{
var layout = await _layoutAccessor.GetLayoutAsync();
var contentZone = layout.Zones["Content"];

await contentZone.AddAsync(await _shapeFactory.CreateAsync("EmailQuotaError"), "0");
}

await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Lombiq.Hosting.Tenants.EmailQuotaManagement.Models;
using Lombiq.Hosting.Tenants.EmailQuotaManagement.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Layout;
using OrchardCore.Mvc.Core.Utilities;
using OrchardCore.Queries.Controllers;
using System.Threading.Tasks;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Filters;

public class EmailSettingsQuotaFilter : IAsyncResultFilter
DemeSzabolcs marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IShapeFactory _shapeFactory;
private readonly ILayoutAccessor _layoutAccessor;
private readonly IQuotaService _quotaService;
private readonly EmailQuotaOptions _emailQuotaOptions;

public EmailSettingsQuotaFilter(
IShapeFactory shapeFactory,
ILayoutAccessor layoutAccessor,
IQuotaService quotaService,
IOptions<EmailQuotaOptions> emailQuotaOptions)
{
_shapeFactory = shapeFactory;
_layoutAccessor = layoutAccessor;
_quotaService = quotaService;
_emailQuotaOptions = emailQuotaOptions.Value;
}

public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (!context.IsAdmin())
{
await next();
return;
}

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.Settings)}" &&
actionRouteValue is nameof(AdminController.Index) &&
context.Result is ViewResult &&
context.RouteData.Values.TryGetValue("GroupId", out var groupId) &&
(string)groupId == "email" &&
_quotaService.ShouldLimitEmails())
{
var layout = await _layoutAccessor.GetLayoutAsync();
var contentZone = layout.Zones["Content"];

var quota = await _quotaService.GetCurrentQuotaAsync();
await contentZone.AddAsync(
await _shapeFactory.CreateAsync("EmailSettingsQuota", new
{
CurrentEmailCount = quota.CurrentEmailQuotaCount,
_emailQuotaOptions.EmailQuota,
}),
"0");
}

await next();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Lombiq.Hosting.Tenants.EmailQuotaManagement.Models;
using System;
using YesSql.Indexes;

namespace Lombiq.Hosting.Tenants.EmailQuotaManagement.Indexes;

public class EmailQuotaIndex : MapIndex
{
public int CurrentEmailQuotaCount { get; set; }
public DateTime LastReminder { get; set; }
}

public class EmailQuotaIndexProvider : IndexProvider<EmailQuota>
{
public override void Describe(DescribeContext<EmailQuota> context) =>
context.For<EmailQuotaIndex>()
.Map(emailQuota => new EmailQuotaIndex
{
CurrentEmailQuotaCount = emailQuota.CurrentEmailQuotaCount,
LastReminder = emailQuota.LastReminder,
});
}
15 changes: 15 additions & 0 deletions Lombiq.Hosting.Tenants.EmailQuotaManagement/License.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Copyright © 2022, [Lombiq Technologies Ltd.](https://lombiq.com)

All rights reserved.

For more information and requests about licensing please [contact us through our website](https://lombiq.com/contact-us).

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.
Loading