mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
23 changed files with 786 additions and 223 deletions
@ -1,113 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Email; |
|||
using Squidex.Infrastructure.Log; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Invitation.Notifications |
|||
{ |
|||
public sealed class NotificationEmailSender : INotificationEmailSender |
|||
{ |
|||
private readonly IEmailSender emailSender; |
|||
private readonly IEmailUrlGenerator emailUrlGenerator; |
|||
private readonly ISemanticLog log; |
|||
private readonly NotificationEmailTextOptions texts; |
|||
|
|||
public bool IsActive |
|||
{ |
|||
get { return true; } |
|||
} |
|||
|
|||
public NotificationEmailSender( |
|||
IOptions<NotificationEmailTextOptions> texts, |
|||
IEmailSender emailSender, |
|||
IEmailUrlGenerator emailUrlGenerator, |
|||
ISemanticLog log) |
|||
{ |
|||
Guard.NotNull(texts); |
|||
Guard.NotNull(emailSender); |
|||
Guard.NotNull(emailUrlGenerator); |
|||
Guard.NotNull(log); |
|||
|
|||
this.texts = texts.Value; |
|||
this.emailSender = emailSender; |
|||
this.emailUrlGenerator = emailUrlGenerator; |
|||
this.log = log; |
|||
} |
|||
|
|||
public Task SendContributorEmailAsync(IUser assigner, IUser assignee, string appName, bool isCreated) |
|||
{ |
|||
Guard.NotNull(assigner); |
|||
Guard.NotNull(assignee); |
|||
Guard.NotNull(appName); |
|||
|
|||
if (assignee.HasConsent()) |
|||
{ |
|||
return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); |
|||
} |
|||
else |
|||
{ |
|||
return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); |
|||
} |
|||
} |
|||
|
|||
private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(emailBody)) |
|||
{ |
|||
LogWarning("No email subject configured for new users"); |
|||
return; |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(emailSubj)) |
|||
{ |
|||
LogWarning("No email body configured for new users"); |
|||
return; |
|||
} |
|||
|
|||
var appUrl = emailUrlGenerator.GenerateUIUrl(); |
|||
|
|||
emailSubj = Format(emailSubj, assigner, assignee, appUrl, appName); |
|||
emailBody = Format(emailBody, assigner, assignee, appUrl, appName); |
|||
|
|||
await emailSender.SendAsync(assignee.Email, emailSubj, emailBody); |
|||
} |
|||
|
|||
private void LogWarning(string reason) |
|||
{ |
|||
log.LogWarning(w => w |
|||
.WriteProperty("action", "InviteUser") |
|||
.WriteProperty("status", "Failed") |
|||
.WriteProperty("reason", reason)); |
|||
} |
|||
|
|||
private static string Format(string text, IUser assigner, IUser assignee, string uiUrl, string appName) |
|||
{ |
|||
text = text.Replace("$APP_NAME", appName); |
|||
|
|||
if (assigner != null) |
|||
{ |
|||
text = text.Replace("$ASSIGNER_EMAIL", assigner.Email); |
|||
text = text.Replace("$ASSIGNER_NAME", assigner.DisplayName()); |
|||
} |
|||
|
|||
if (assignee != null) |
|||
{ |
|||
text = text.Replace("$ASSIGNEE_EMAIL", assignee.Email); |
|||
text = text.Replace("$ASSIGNEE_NAME", assignee.DisplayName()); |
|||
} |
|||
|
|||
text = text.Replace("$UI_URL", uiUrl); |
|||
|
|||
return text; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Orleans; |
|||
using Orleans.Concurrency; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public interface IUsageNotifierGrain : IGrainWithStringKey |
|||
{ |
|||
[OneWay] |
|||
Task NotifyAsync(UsageNotification notification); |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Orleans; |
|||
using Squidex.Domain.Apps.Core.Apps; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.Tasks; |
|||
using Squidex.Infrastructure.UsageTracking; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public class UsageGate |
|||
{ |
|||
private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
|||
private readonly IAppPlansProvider appPlansProvider; |
|||
private readonly IApiUsageTracker apiUsageTracker; |
|||
private readonly IUsageNotifierGrain usageLimitNotifier; |
|||
|
|||
public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IGrainFactory grainFactory) |
|||
{ |
|||
Guard.NotNull(apiUsageTracker); |
|||
Guard.NotNull(appPlansProvider); |
|||
Guard.NotNull(grainFactory); |
|||
|
|||
this.appPlansProvider = appPlansProvider; |
|||
this.apiUsageTracker = apiUsageTracker; |
|||
|
|||
usageLimitNotifier = grainFactory.GetGrain<IUsageNotifierGrain>(SingleGrain.Id); |
|||
} |
|||
|
|||
public virtual async Task<bool> IsBlockedAsync(IAppEntity app, DateTime today) |
|||
{ |
|||
Guard.NotNull(app); |
|||
|
|||
var isLocked = false; |
|||
|
|||
var (plan, _) = appPlansProvider.GetPlanForApp(app); |
|||
|
|||
if (plan.MaxApiCalls > 0 || plan.BlockingApiCalls > 0) |
|||
{ |
|||
var appId = app.Id; |
|||
|
|||
var usage = await apiUsageTracker.GetMonthCostsAsync(appId.ToString(), today); |
|||
|
|||
if (IsAboutToBeLocked(today, plan.MaxApiCalls, usage) && !HasNotifiedBefore(app.Id)) |
|||
{ |
|||
var users = app.Contributors.Where(x => x.Value == Role.Owner).Select(x => x.Key).ToArray(); |
|||
|
|||
var notification = new UsageNotification |
|||
{ |
|||
AppId = appId, |
|||
AppName = app.Name, |
|||
Usage = usage, |
|||
UsageLimit = plan.BlockingApiCalls, |
|||
Users = users |
|||
}; |
|||
|
|||
usageLimitNotifier.NotifyAsync(notification).Forget(); |
|||
|
|||
TrackNotified(appId); |
|||
} |
|||
|
|||
isLocked = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; |
|||
} |
|||
|
|||
return isLocked; |
|||
} |
|||
|
|||
private bool HasNotifiedBefore(Guid appId) |
|||
{ |
|||
return memoryCache.Get<bool>(appId); |
|||
} |
|||
|
|||
private bool TrackNotified(Guid appId) |
|||
{ |
|||
return memoryCache.Set(appId, true, TimeSpan.FromHours(1)); |
|||
} |
|||
|
|||
private bool IsAboutToBeLocked(DateTime today, long limit, long usage) |
|||
{ |
|||
var daysInMonth = DateTime.DaysInMonth(today.Year, today.Month); |
|||
|
|||
var forecasted = ((float)usage / today.Day) * daysInMonth; |
|||
|
|||
return forecasted > limit; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public sealed class UsageNotification |
|||
{ |
|||
public Guid AppId { get; set; } |
|||
|
|||
public string AppName { get; set; } |
|||
|
|||
public long Usage { get; set; } |
|||
|
|||
public long UsageLimit { get; set; } |
|||
|
|||
public string[] Users { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Entities.Apps.Notifications; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Email; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.States; |
|||
using Squidex.Infrastructure.Tasks; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public sealed class UsageNotifierGrain : GrainOfString, IUsageNotifierGrain |
|||
{ |
|||
private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromHours(12); |
|||
private readonly IGrainState<State> state; |
|||
private readonly INotificationSender notificationSender; |
|||
private readonly IUserResolver userResolver; |
|||
|
|||
[CollectionName("UsageNotifications")] |
|||
public sealed class State |
|||
{ |
|||
public Dictionary<Guid, DateTime> NotificationsSent { get; } = new Dictionary<Guid, DateTime>(); |
|||
} |
|||
|
|||
public UsageNotifierGrain(IGrainState<State> state, INotificationSender notificationSender, IUserResolver userResolver) |
|||
{ |
|||
Guard.NotNull(state); |
|||
Guard.NotNull(notificationSender); |
|||
Guard.NotNull(userResolver); |
|||
|
|||
this.state = state; |
|||
this.notificationSender = notificationSender; |
|||
this.userResolver = userResolver; |
|||
} |
|||
|
|||
public async Task NotifyAsync(UsageNotification notification) |
|||
{ |
|||
var now = DateTime.UtcNow; |
|||
|
|||
if (!HasBeenSentBefore(notification.AppId, now)) |
|||
{ |
|||
if (notificationSender.IsActive) |
|||
{ |
|||
foreach (var userId in notification.Users) |
|||
{ |
|||
var user = await userResolver.FindByIdOrEmailAsync(userId); |
|||
|
|||
if (user != null) |
|||
{ |
|||
notificationSender.SendUsageAsync(user, |
|||
notification.AppName, |
|||
notification.Usage, |
|||
notification.UsageLimit).Forget(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
await TrackNotifiedAsync(notification.AppId, now); |
|||
} |
|||
} |
|||
|
|||
private bool HasBeenSentBefore(Guid appId, DateTime now) |
|||
{ |
|||
if (state.Value.NotificationsSent.TryGetValue(appId, out var lastSent)) |
|||
{ |
|||
var elapsed = now - lastSent; |
|||
|
|||
return elapsed > TimeBetweenNotifications; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
private Task TrackNotifiedAsync(Guid appId, DateTime now) |
|||
{ |
|||
state.Value.NotificationsSent[appId] = now; |
|||
|
|||
return state.WriteAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Email; |
|||
using Squidex.Infrastructure.Log; |
|||
using Squidex.Shared.Users; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Notifications |
|||
{ |
|||
public sealed class NotificationEmailSender : INotificationSender |
|||
{ |
|||
private readonly IEmailSender emailSender; |
|||
private readonly IEmailUrlGenerator emailUrlGenerator; |
|||
private readonly ISemanticLog log; |
|||
private readonly NotificationEmailTextOptions texts; |
|||
|
|||
private sealed class TemplatesVars |
|||
{ |
|||
public IUser User { get; set; } |
|||
|
|||
public IUser? Assigner { get; set; } |
|||
|
|||
public string AppName { get; set; } |
|||
|
|||
public long? ApiCalls { get; set; } |
|||
|
|||
public long? ApiCallsLimit { get; set; } |
|||
|
|||
public string URL { get; set; } |
|||
} |
|||
|
|||
public bool IsActive |
|||
{ |
|||
get { return true; } |
|||
} |
|||
|
|||
public NotificationEmailSender( |
|||
IOptions<NotificationEmailTextOptions> texts, |
|||
IEmailSender emailSender, |
|||
IEmailUrlGenerator emailUrlGenerator, |
|||
ISemanticLog log) |
|||
{ |
|||
Guard.NotNull(texts); |
|||
Guard.NotNull(emailSender); |
|||
Guard.NotNull(emailUrlGenerator); |
|||
Guard.NotNull(log); |
|||
|
|||
this.texts = texts.Value; |
|||
this.emailSender = emailSender; |
|||
this.emailUrlGenerator = emailUrlGenerator; |
|||
this.log = log; |
|||
} |
|||
|
|||
public Task SendUsageAsync(IUser user, string appName, long usage, long usageLimit) |
|||
{ |
|||
Guard.NotNull(user); |
|||
Guard.NotNull(appName); |
|||
|
|||
var vars = new TemplatesVars |
|||
{ |
|||
ApiCalls = usage, |
|||
ApiCallsLimit = usageLimit, |
|||
AppName = appName |
|||
}; |
|||
|
|||
return SendEmailAsync("Usage", |
|||
texts.UsageSubject, |
|||
texts.UsageBody, |
|||
user, vars); |
|||
} |
|||
|
|||
public Task SendInviteAsync(IUser assigner, IUser user, string appName) |
|||
{ |
|||
Guard.NotNull(assigner); |
|||
Guard.NotNull(user); |
|||
Guard.NotNull(appName); |
|||
|
|||
var vars = new TemplatesVars { Assigner = assigner, AppName = appName }; |
|||
|
|||
if (user.HasConsent()) |
|||
{ |
|||
return SendEmailAsync("ExistingUser", |
|||
texts.ExistingUserSubject, |
|||
texts.ExistingUserBody, |
|||
user, vars); |
|||
} |
|||
else |
|||
{ |
|||
return SendEmailAsync("NewUser", |
|||
texts.NewUserSubject, |
|||
texts.NewUserBody, |
|||
user, vars); |
|||
} |
|||
} |
|||
|
|||
private async Task SendEmailAsync(string template, string emailSubj, string emailBody, IUser user, TemplatesVars vars) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(emailBody)) |
|||
{ |
|||
LogWarning($"No email subject configured for {template}"); |
|||
return; |
|||
} |
|||
|
|||
if (string.IsNullOrWhiteSpace(emailSubj)) |
|||
{ |
|||
LogWarning($"No email body configured for {template}"); |
|||
return; |
|||
} |
|||
|
|||
vars.URL = emailUrlGenerator.GenerateUIUrl(); |
|||
|
|||
vars.User = user; |
|||
|
|||
emailSubj = Format(emailSubj, vars); |
|||
emailBody = Format(emailBody, vars); |
|||
|
|||
try |
|||
{ |
|||
await emailSender.SendAsync(user.Email, emailSubj, emailBody); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
log.LogError(ex, w => w |
|||
.WriteProperty("action", "SendNotification") |
|||
.WriteProperty("status", "Failed")); |
|||
|
|||
throw; |
|||
} |
|||
} |
|||
|
|||
private void LogWarning(string reason) |
|||
{ |
|||
log.LogWarning(w => w |
|||
.WriteProperty("action", "SendNotification") |
|||
.WriteProperty("status", "Failed") |
|||
.WriteProperty("reason", reason)); |
|||
} |
|||
|
|||
private static string Format(string text, TemplatesVars vars) |
|||
{ |
|||
text = text.Replace("$APP_NAME", vars.AppName); |
|||
|
|||
if (vars.Assigner != null) |
|||
{ |
|||
text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email); |
|||
text = text.Replace("$ASSIGNER_NAME", vars.Assigner.DisplayName()); |
|||
} |
|||
|
|||
if (vars.User != null) |
|||
{ |
|||
text = text.Replace("$USER_EMAIL", vars.User.Email); |
|||
text = text.Replace("$USER_NAME", vars.User.DisplayName()); |
|||
} |
|||
|
|||
if (vars.ApiCallsLimit != null) |
|||
{ |
|||
text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString()); |
|||
} |
|||
|
|||
if (vars.ApiCalls != null) |
|||
{ |
|||
text = text.Replace("$API_CALLS", vars.ApiCalls.ToString()); |
|||
} |
|||
|
|||
text = text.Replace("$UI_URL", vars.URL); |
|||
|
|||
return text; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
// ==========================================================================
|
|||
// 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.Domain.Apps.Entities.TestHelpers; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Orleans; |
|||
using Squidex.Infrastructure.UsageTracking; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Plans |
|||
{ |
|||
public class UsageGateTests |
|||
{ |
|||
private readonly IAppEntity appEntity; |
|||
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>(); |
|||
private readonly IAppPlansProvider appPlansProvider = A.Fake<IAppPlansProvider>(); |
|||
private readonly IApiUsageTracker usageTracker = A.Fake<IApiUsageTracker>(); |
|||
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); |
|||
private readonly IUsageNotifierGrain usageNotifierGrain = A.Fake<IUsageNotifierGrain>(); |
|||
private readonly DateTime today = new DateTime(2020, 04, 10); |
|||
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); |
|||
private readonly UsageGate sut; |
|||
private long apiCallsBlocking; |
|||
private long apiCallsMax; |
|||
private long apiCallsCurrent; |
|||
|
|||
public UsageGateTests() |
|||
{ |
|||
appEntity = Mocks.App(appId); |
|||
|
|||
A.CallTo(() => grainFactory.GetGrain<IUsageNotifierGrain>(SingleGrain.Id, null)) |
|||
.Returns(usageNotifierGrain); |
|||
|
|||
A.CallTo(() => appPlansProvider.GetPlan(null)) |
|||
.Returns(appPlan); |
|||
|
|||
A.CallTo(() => appPlansProvider.GetPlanForApp(appEntity)) |
|||
.Returns((appPlan, "free")); |
|||
|
|||
A.CallTo(() => appPlan.MaxApiCalls) |
|||
.ReturnsLazily(x => apiCallsMax); |
|||
|
|||
A.CallTo(() => appPlan.BlockingApiCalls) |
|||
.ReturnsLazily(x => apiCallsBlocking); |
|||
|
|||
A.CallTo(() => usageTracker.GetMonthCostsAsync(appId.Id.ToString(), today)) |
|||
.ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); |
|||
|
|||
sut = new UsageGate(appPlansProvider, usageTracker, grainFactory); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_true_if_over_blocking_limt() |
|||
{ |
|||
apiCallsCurrent = 1000; |
|||
apiCallsBlocking = 600; |
|||
apiCallsMax = 600; |
|||
|
|||
var isBlocked = await sut.IsBlockedAsync(appEntity, today); |
|||
|
|||
Assert.True(isBlocked); |
|||
|
|||
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_false_if_below_blocking_limit() |
|||
{ |
|||
apiCallsCurrent = 100; |
|||
apiCallsBlocking = 1600; |
|||
apiCallsMax = 1600; |
|||
|
|||
var isBlocked = await sut.IsBlockedAsync(appEntity, today); |
|||
|
|||
Assert.False(isBlocked); |
|||
|
|||
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) |
|||
.MustNotHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_false_and_notify_if_about_to_over_included_contingent() |
|||
{ |
|||
apiCallsCurrent = 1200; // * 30 days = 3600
|
|||
apiCallsBlocking = 5000; |
|||
apiCallsMax = 3000; |
|||
|
|||
var isBlocked = await sut.IsBlockedAsync(appEntity, today); |
|||
|
|||
Assert.False(isBlocked); |
|||
|
|||
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_return_false_and_notify_if_about_to_over_included_contingent_but_no_max_given() |
|||
{ |
|||
apiCallsCurrent = 1200; // * 30 days = 3600
|
|||
apiCallsBlocking = 5000; |
|||
apiCallsMax = 0; |
|||
|
|||
var isBlocked = await sut.IsBlockedAsync(appEntity, today); |
|||
|
|||
Assert.False(isBlocked); |
|||
|
|||
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) |
|||
.MustHaveHappened(); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_only_notify_once_if_about_to_be_over_included_contingent() |
|||
{ |
|||
apiCallsCurrent = 1200; // * 30 days = 3600
|
|||
apiCallsBlocking = 5000; |
|||
apiCallsMax = 3000; |
|||
|
|||
await sut.IsBlockedAsync(appEntity, today); |
|||
await sut.IsBlockedAsync(appEntity, today); |
|||
|
|||
A.CallTo(() => usageNotifierGrain.NotifyAsync(A<UsageNotification>.Ignored)) |
|||
.MustHaveHappenedOnceExactly(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Options; |
|||
using Xunit; |
|||
|
|||
namespace Squidex.Infrastructure.Email |
|||
{ |
|||
public class SmtpEmailSenderTests |
|||
{ |
|||
[Fact] |
|||
public async Task Should_handle_timeout_properly() |
|||
{ |
|||
var sut = new SmtpEmailSender(Options.Create(new SmtpOptions |
|||
{ |
|||
Sender = "sebastian@squidex.io", |
|||
Server = "invalid", |
|||
Timeout = 1000 |
|||
})); |
|||
|
|||
var timer = Task.Delay(5000); |
|||
|
|||
var result = await Task.WhenAny(timer, sut.SendAsync("hello@squidex.io", "TEST", "TEST")); |
|||
|
|||
Assert.NotSame(timer, result); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue