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