diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs index 28999ecb9..58111c21f 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs @@ -55,13 +55,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public async Task On(Envelope @event) { - if (@event.Payload is AppContributorAssigned appContributorAssigned && appContributorAssigned.Actor.IsClient) + if (@event.Payload is AppContributorAssigned appContributorAssigned && appContributorAssigned.Actor.IsSubject) { - var assigner = await userResolver.FindByIdOrEmailAsync(appContributorAssigned.Actor.Identifier); + var assignerId = appContributorAssigned.Actor.Identifier; + var assigneeId = appContributorAssigned.ContributorId; + + var assigner = await userResolver.FindByIdOrEmailAsync(assignerId); if (assigner == null) { - LogWarning($"Assigner {appContributorAssigned.Actor.Identifier} not found"); + LogWarning($"Assigner {assignerId} not found"); return; } @@ -69,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation if (assignee == null) { - LogWarning($"Assignee {appContributorAssigned.ContributorId} not found"); + LogWarning($"Assignee {assigneeId} not found"); return; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailSender.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailSender.cs index 62cf4a73c..ac92cdc63 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailSender.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailSender.cs @@ -40,23 +40,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public Task SendExistingUserEmailAsync(IUser assigner, IUser assignee, string appName) { - return SendEmailAsync(texts.NewUserSubject, texts.NewUserBody, assigner, assignee, appName); + return SendEmailAsync(texts.ExistingUserSubject, texts.ExistingUserBody, assigner, assignee, appName); } public Task SendNewUserEmailAsync(IUser assigner, IUser assignee, string appName) { - return SendEmailAsync(texts.ExistingUserBody, texts.ExistingUserSubject, assigner, assignee, appName); + return SendEmailAsync(texts.NewUserBody, texts.NewUserSubject, assigner, assignee, appName); } - private async Task SendEmailAsync(string emailBody, string emailSubj, IUser assigner, IUser assignee, string appName) + private async Task SendEmailAsync(string emailSubj, string emailBody, IUser assigner, IUser assignee, string appName) { - if (string.IsNullOrWhiteSpace(texts.NewUserSubject)) + if (string.IsNullOrWhiteSpace(emailBody)) { LogWarning("No email subject configured for new users"); return; } - if (string.IsNullOrWhiteSpace(texts.NewUserBody)) + if (string.IsNullOrWhiteSpace(emailSubj)) { LogWarning("No email body configured for new users"); return; diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 28ad6cdc3..0c54dad3d 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -103,7 +103,7 @@ /* * The email body when a new user is added as contributor. */ - "newUserBody": "Welcome\n\nYou have been invited to Squidex CMS by $ASSIGNER_NAME ($ASSIGNER_EMAIL) and to app $", + "newUserBody": "You have been invited to join an app at Squidex CMS\n\nWelcome to Squidex\n$ASSIGNER_NAME ($ASSIGNER_EMAIL) has invited you to join app $APP_NAME at Squidex headless CMS.\nLogin with your Github, Google or Microsoft account to create a new user account and start editing content now.\n\nThank you very much,\nThe Squidex Team\n<> [$UI_URL]", /* * The email subject when an existing user is added as contributor. */ @@ -111,7 +111,7 @@ /* * The email body when an existing user is added as contributor. */ - "existingUserBody": "" + "existingUserBody": "You have been invited to join an app at Squidex CMS\n\nWelcome to Squidex\n$ASSIGNER_NAME ($ASSIGNER_EMAIL) has invited you to join app $APP_NAME at Squidex headless CMS.\nLogin or reload the Management UI to see the app.\n\nThank you very much,\nThe Squidex Team\n<> [$UI_URL]" } }, diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailEventConsumerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailEventConsumerTests.cs new file mode 100644 index 000000000..c2134ea1a --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailEventConsumerTests.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// 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 Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitation +{ + public class InvitationEmailEventConsumerTests + { + private readonly IInvitationEmailSender emailSender = A.Fake(); + private readonly IUserResolver userResolver = A.Fake(); + private readonly IUser assigner = A.Fake(); + private readonly IUser assignee = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string assignerId = Guid.NewGuid().ToString(); + private readonly string assigneeId = Guid.NewGuid().ToString(); + private readonly string appName = "my-app"; + private readonly InvitationEmailEventConsumer sut; + + public InvitationEmailEventConsumerTests() + { + sut = new InvitationEmailEventConsumer(emailSender, userResolver, log); + } + + [Fact] + public async Task Should_ignore_contributors_assigned_by_clients() + { + var @event = Envelope.Create(CreateEvent(RefTokenType.Client, true)); + + await sut.On(@event); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A.Ignored)) + .MustNotHaveHappened(); + + A.CallTo(() => emailSender.SendNewUserEmailAsync(A.Ignored, A.Ignored, appName)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_send_email_if_assigner_not_found() + { + var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendNewUserEmailAsync(A.Ignored, A.Ignored, appName)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_email_if_assignee_not_found() + { + var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(Task.FromResult(null)); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendNewUserEmailAsync(A.Ignored, A.Ignored, appName)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_send_email_for_new_user() + { + var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, false)); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(assignee); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendNewUserEmailAsync(assigner, assignee, appName)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_send_email_for_existing_user() + { + var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) + .Returns(assigner); + + A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) + .Returns(assignee); + + await sut.On(@event); + + A.CallTo(() => emailSender.SendExistingUserEmailAsync(assigner, assignee, appName)) + .MustNotHaveHappened(); + } + + private void MustLogWarning() + { + A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + + private IEvent CreateEvent(string assignerType, bool isNew) + { + return new AppContributorAssigned + { + Actor = new RefToken(assignerType, assignerId), + AppId = new NamedId(Guid.NewGuid(), appName), + ContributorId = assigneeId, + IsCreated = isNew + }; + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailSenderTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailSenderTests.cs new file mode 100644 index 000000000..9c50f13fc --- /dev/null +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailSenderTests.cs @@ -0,0 +1,140 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Email; +using Squidex.Infrastructure.Log; +using Squidex.Shared.Identity; +using Squidex.Shared.Users; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps.Invitation +{ + public class InvitationEmailSenderTests + { + private readonly IEmailSender emailSender = A.Fake(); + private readonly IEmailUrlGenerator emailUrlGenerator = A.Fake(); + private readonly IUser assigner = A.Fake(); + private readonly IUser assignee = A.Fake(); + private readonly ISemanticLog log = A.Fake(); + private readonly string appName = "my-app"; + private readonly string uiUrl = "my-ui"; + private readonly InvitationEmailTextOptions texts = new InvitationEmailTextOptions(); + private readonly InvitationEmailSender sut; + + public InvitationEmailSenderTests() + { + A.CallTo(() => assigner.Email) + .Returns("sebastian@squidex.io"); + A.CallTo(() => assigner.Claims) + .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Sebastian Stehle") }); + + A.CallTo(() => assignee.Email) + .Returns("qaisar@squidex.io"); + A.CallTo(() => assignee.Claims) + .Returns(new List { new Claim(SquidexClaimTypes.DisplayName, "Qaisar Ahmad") }); + + A.CallTo(() => emailUrlGenerator.GenerateUIUrl()) + .Returns(uiUrl); + + sut = new InvitationEmailSender(Options.Create(texts), emailSender, emailUrlGenerator, log); + } + + [Fact] + public async Task Should_format_assigner_email_and_send_email() + { + await TestFormattingAsync("Email: $ASSIGNER_EMAIL", "Email: sebastian@squidex.io"); + } + + [Fact] + public async Task Should_format_assignee_email_and_send_email() + { + await TestFormattingAsync("Email: $ASSIGNEE_EMAIL", "Email: qaisar@squidex.io"); + } + + [Fact] + public async Task Should_format_assigner_name_and_send_email() + { + await TestFormattingAsync("Name: $ASSIGNER_NAME", "Name: Sebastian Stehle"); + } + + [Fact] + public async Task Should_format_assignee_name_and_send_email() + { + await TestFormattingAsync("Name: $ASSIGNEE_NAME", "Name: Qaisar Ahmad"); + } + + [Fact] + public async Task Should_format_app_name_and_send_email() + { + await TestFormattingAsync("App: $APP_NAME", "App: my-app"); + } + + [Fact] + public async Task Should_format_ui_url_and_send_email() + { + await TestFormattingAsync("UI: $UI_URL", "UI: my-ui"); + } + + [Fact] + public async Task Should_not_send_email_if_texts_for_new_user_are_empty() + { + await sut.SendNewUserEmailAsync(assigner, assignee, appName); + + A.CallTo(() => emailSender.SendAsync(assignee.Email, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_not_send_email_if_texts_for_existing_user_are_empty() + { + await sut.SendExistingUserEmailAsync(assigner, assignee, appName); + + A.CallTo(() => emailSender.SendAsync(assignee.Email, A.Ignored, A.Ignored)) + .MustNotHaveHappened(); + + MustLogWarning(); + } + + [Fact] + public async Task Should_send_email_for_existing_user() + { + texts.ExistingUserSubject = "email-subject"; + texts.ExistingUserBody = "email-body"; + + await sut.SendExistingUserEmailAsync(assigner, assignee, appName); + + A.CallTo(() => emailSender.SendAsync(assignee.Email, "email-subject", "email-body")) + .MustHaveHappened(); + } + + private async Task TestFormattingAsync(string pattern, string result) + { + texts.NewUserSubject = pattern; + texts.NewUserBody = pattern; + + await sut.SendNewUserEmailAsync(assigner, assignee, appName); + + A.CallTo(() => emailSender.SendAsync(assignee.Email, result, result)) + .MustHaveHappened(); + } + + private void MustLogWarning() + { + A.CallTo(() => log.Log(SemanticLogLevel.Warning, A.Ignored, A>.Ignored)) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/InviteUserCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs similarity index 98% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/InviteUserCommandMiddlewareTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs index 9db5dca19..3e71cfee5 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/InviteUserCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps.Invitation { public class InviteUserCommandMiddlewareTests {