Browse Source

Merge branch 'feature/invitations'

pull/355/head
Sebastian Stehle 8 years ago
parent
commit
d23677198e
  1. 17
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  3. 24
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs
  4. 2
      src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs
  5. 6
      src/Squidex/appsettings.json
  6. 22
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs
  7. 20
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  8. 120
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailEventConsumerTests.cs

17
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -67,12 +67,12 @@ namespace Squidex.Domain.Apps.Entities.Apps
Create(c); Create(c);
}); });
case AssignContributor assigneContributor: case AssignContributor assignContributor:
return UpdateReturnAsync(assigneContributor, async c => return UpdateReturnAsync(assignContributor, async c =>
{ {
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId), Snapshot.Roles); await GuardAppContributors.CanAssign(Snapshot.Contributors, Snapshot.Roles, c, userResolver, GetPlan());
AssignContributor(c); AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId));
return EntityCreatedResult.Create(c.ContributorId, Version); return EntityCreatedResult.Create(c.ContributorId, Version);
}); });
@ -218,6 +218,11 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
} }
private IAppLimitsPlan GetPlan()
{
return appPlansProvider.GetPlan(Snapshot.Plan?.PlanId);
}
public void Create(CreateApp command) public void Create(CreateApp command)
{ {
var appId = NamedId.Of(command.AppId, command.Name); var appId = NamedId.Of(command.AppId, command.Name);
@ -261,9 +266,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated())); RaiseEvent(SimpleMapper.Map(command, new AppLanguageUpdated()));
} }
public void AssignContributor(AssignContributor command) public void AssignContributor(AssignContributor command, bool isNew)
{ {
RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned())); RaiseEvent(SimpleMapper.Map(command, new AppContributorAssigned { IsNew = isNew }));
} }
public void RemoveContributor(RemoveContributor command) public void RemoveContributor(RemoveContributor command)

2
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
public static class GuardAppContributors public static class GuardAppContributors
{ {
public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan, Roles roles) public static Task CanAssign(AppContributors contributors, Roles roles, AssignContributor command, IUserResolver users, IAppLimitsPlan plan)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));

24
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitationEmailEventConsumer.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -17,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
{ {
public sealed class InvitationEmailEventConsumer : IEventConsumer public sealed class InvitationEmailEventConsumer : IEventConsumer
{ {
private static readonly Duration MaxAge = Duration.FromDays(2);
private readonly IInvitationEmailSender emailSender; private readonly IInvitationEmailSender emailSender;
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
private readonly ISemanticLog log; private readonly ISemanticLog log;
@ -60,8 +63,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
return; return;
} }
if (@event.Payload is AppContributorAssigned appContributorAssigned && appContributorAssigned.Actor.IsSubject) if (@event.Headers.EventStreamNumber() <= 1)
{ {
return;
}
var now = SystemClock.Instance.GetCurrentInstant();
var timestamp = @event.Headers.Timestamp();
if (now - timestamp > MaxAge)
{
return;
}
if (@event.Payload is AppContributorAssigned appContributorAssigned)
{
if (!appContributorAssigned.Actor.IsSubject || !appContributorAssigned.IsNew)
{
return;
}
var assignerId = appContributorAssigned.Actor.Identifier; var assignerId = appContributorAssigned.Actor.Identifier;
var assigneeId = appContributorAssigned.ContributorId; var assigneeId = appContributorAssigned.ContributorId;

2
src/Squidex.Domain.Apps.Events/Apps/AppContributorAssigned.cs

@ -17,5 +17,7 @@ namespace Squidex.Domain.Apps.Events.Apps
public string Role { get; set; } public string Role { get; set; }
public bool IsCreated { get; set; } public bool IsCreated { get; set; }
public bool IsNew { get; set; }
} }
} }

6
src/Squidex/appsettings.json

@ -104,15 +104,15 @@
/* /*
* The email body when a new user is added as contributor. * The email body when a new user is added as contributor.
*/ */
"newUserBody": "Welcome to Squidex\r\nDear User,\r\n\r\n{{var:assigner_name}} ($ASSIGNER_EMAIL) has invited you to join Project (also called an App) {{var:app_name}} at Squidex Headless CMS. Login with your Github, Google or Microsoft credentials to create a new user account and start editing content now.\r\n\r\nThank you very much,\r\nThe Squidex Team\r\n\r\n<<Start now!>> [https://cloud.squidex.io]", "newUserBody": "Welcome to Squidex\r\nDear User,\r\n\r\n{{$ASSIGNER_NAME}} ($ASSIGNER_EMAIL) has invited you to join Project (also called an App) {{var:app_name}} at Squidex Headless CMS. Login with your Github, Google or Microsoft credentials to create a new user account and start editing content now.\r\n\r\nThank you very much,\r\nThe Squidex Team\r\n\r\n<<Start now!>> [$UI_URL]",
/* /*
* The email subject when an existing user is added as contributor. * The email subject when an existing user is added as contributor.
*/ */
"existingUserSubject": "[Squidex CMS] You have been invited to App $APP_NAME", "existingUserSubject": "[Squidex CMS] You have been invited to join App $APP_NAME",
/* /*
* The email body when an existing user is added as contributor. * The email body when an existing user is added as contributor.
*/ */
"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<<Start now!>> [$UI_URL]" "existingUserBody": "Dear User,\r\n\r\n{{$ASSIGNER_NAME}} ($ASSIGNER_EMAIL) has invited you to join App {{var:app_name}} at Squidex Headless CMS.\r\n\r\nLogin or reload the Management UI to see the App\r\n\r\nThank you very much,\r\nThe Squidex Team\r\n\r\n<<Start now!>> [$UI_URL]"
} }
}, },

22
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs

@ -171,7 +171,27 @@ namespace Squidex.Domain.Apps.Entities.Apps
LastEvents LastEvents
.ShouldHaveSameEvents( .ShouldHaveSameEvents(
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor }) CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Editor, IsNew = true })
);
}
[Fact]
public async Task AssignContributor_should_create_update_events_and_update_state()
{
var command = new AssignContributor { ContributorId = contributorId, Role = Role.Owner };
await ExecuteCreateAsync();
await ExecuteAssignContributorAsync();
var result = await sut.ExecuteAsync(CreateCommand(command));
result.ShouldBeEquivalent(EntityCreatedResult.Create(contributorId, 6));
Assert.Equal(Role.Owner, sut.Snapshot.Contributors[contributorId]);
LastEvents
.ShouldHaveSameEvents(
CreateEvent(new AppContributorAssigned { ContributorId = contributorId, Role = Role.Owner })
); );
} }

20
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor(); var command = new AssignContributor();
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan),
new ValidationError("Contributor id is required.", "ContributorId")); new ValidationError("Contributor id is required.", "ContributorId"));
} }
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor { ContributorId = "1", Role = "Invalid" }; var command = new AssignContributor { ContributorId = "1", Role = "Invalid" };
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan),
new ValidationError("Role is not a valid value.", "Role")); new ValidationError("Role is not a valid value.", "Role"));
} }
@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan, roles), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan),
new ValidationError("Contributor has already this role.", "Role")); new ValidationError("Contributor has already this role.", "Role"));
} }
@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner }; var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner };
return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles)); return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan));
} }
[Fact] [Fact]
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") }; var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") };
return Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles)); return Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan));
} }
[Fact] [Fact]
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Owner); var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", Role.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan, roles), return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan),
new ValidationError("You have reached the maximum number of contributors for your plan.")); new ValidationError("You have reached the maximum number of contributors for your plan."));
} }
@ -115,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor { ContributorId = "1@email.com" }; var command = new AssignContributor { ContributorId = "1@email.com" };
await GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles); await GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan);
Assert.Equal("1", command.ContributorId); Assert.Equal("1", command.ContributorId);
} }
@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
var command = new AssignContributor { ContributorId = "1" }; var command = new AssignContributor { ContributorId = "1" };
return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles); return GuardAppContributors.CanAssign(contributors_0, roles, command, users, appPlan);
} }
[Fact] [Fact]
@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Editor); var contributors_1 = contributors_0.Assign("1", Role.Editor);
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan, roles); return GuardAppContributors.CanAssign(contributors_1, roles, command, users, appPlan);
} }
[Fact] [Fact]
@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Editor); var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_2 = contributors_1.Assign("2", Role.Editor); var contributors_2 = contributors_1.Assign("2", Role.Editor);
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan, roles); return GuardAppContributors.CanAssign(contributors_2, roles, command, users, appPlan);
} }
[Fact] [Fact]

120
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEmailEventConsumerTests.cs

@ -8,6 +8,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using NodaTime;
using Squidex.Domain.Apps.Events.Apps; using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -34,88 +35,104 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
A.CallTo(() => emailSender.IsActive) A.CallTo(() => emailSender.IsActive)
.Returns(true); .Returns(true);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(assigner);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId))
.Returns(assignee);
sut = new InvitationEmailEventConsumer(emailSender, userResolver, log); sut = new InvitationEmailEventConsumer(emailSender, userResolver, log);
} }
[Fact] [Fact]
public async Task Should_ignore_contributors_assigned_by_clients() public async Task Should_not_send_email_if_contributors_assigned_by_clients()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Client, true)); var @event = CreateEvent(RefTokenType.Client, true);
await sut.On(@event); await sut.On(@event);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored)) MustNotResolveUser();
.MustNotHaveHappened(); MustNotSendEmail();
}
A.CallTo(() => emailSender.SendNewUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, appName)) [Fact]
.MustNotHaveHappened(); public async Task Should_not_send_email_for_initial_owner()
{
var @event = CreateEvent(RefTokenType.Subject, false, streamNumber: 1);
await sut.On(@event);
MustNotSendEmail();
}
[Fact]
public async Task Should_not_send_email_for_old_events()
{
var @event = CreateEvent(RefTokenType.Subject, true, instant: SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromHours(50)));
await sut.On(@event);
MustNotResolveUser();
MustNotSendEmail();
}
[Fact]
public async Task Should_not_send_email_for_old_contributor()
{
var @event = CreateEvent(RefTokenType.Subject, true, isNewContributor: false);
await sut.On(@event);
MustNotResolveUser();
MustNotSendEmail();
} }
[Fact] [Fact]
public async Task Should_not_send_email_if_sender_not_active() public async Task Should_not_send_email_if_sender_not_active()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); var @event = CreateEvent(RefTokenType.Subject, true);
A.CallTo(() => emailSender.IsActive) A.CallTo(() => emailSender.IsActive)
.Returns(false); .Returns(false);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(Task.FromResult<IUser>(null));
await sut.On(@event); await sut.On(@event);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored)) MustNotResolveUser();
.MustNotHaveHappened(); MustNotSendEmail();
A.CallTo(() => emailSender.SendNewUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, appName))
.MustNotHaveHappened();
} }
[Fact] [Fact]
public async Task Should_not_send_email_if_assigner_not_found() public async Task Should_not_send_email_if_assigner_not_found()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); var @event = CreateEvent(RefTokenType.Subject, true);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId)) A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(Task.FromResult<IUser>(null)); .Returns(Task.FromResult<IUser>(null));
await sut.On(@event); await sut.On(@event);
A.CallTo(() => emailSender.SendNewUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, appName)) MustNotSendEmail();
.MustNotHaveHappened();
MustLogWarning(); MustLogWarning();
} }
[Fact] [Fact]
public async Task Should_not_send_email_if_assignee_not_found() public async Task Should_not_send_email_if_assignee_not_found()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); var @event = CreateEvent(RefTokenType.Subject, true);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(assigner);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId)) A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId))
.Returns(Task.FromResult<IUser>(null)); .Returns(Task.FromResult<IUser>(null));
await sut.On(@event); await sut.On(@event);
A.CallTo(() => emailSender.SendNewUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, appName)) MustNotSendEmail();
.MustNotHaveHappened();
MustLogWarning(); MustLogWarning();
} }
[Fact] [Fact]
public async Task Should_send_email_for_new_user() public async Task Should_send_email_for_new_user()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, true)); var @event = CreateEvent(RefTokenType.Subject, true);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(assigner);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId))
.Returns(assignee);
await sut.On(@event); await sut.On(@event);
@ -126,13 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
[Fact] [Fact]
public async Task Should_send_email_for_existing_user() public async Task Should_send_email_for_existing_user()
{ {
var @event = Envelope.Create(CreateEvent(RefTokenType.Subject, false)); var @event = CreateEvent(RefTokenType.Subject, false);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assignerId))
.Returns(assigner);
A.CallTo(() => userResolver.FindByIdOrEmailAsync(assigneeId))
.Returns(assignee);
await sut.On(@event); await sut.On(@event);
@ -146,15 +157,38 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
.MustHaveHappened(); .MustHaveHappened();
} }
private IEvent CreateEvent(string assignerType, bool isNew) private void MustNotResolveUser()
{ {
return new AppContributorAssigned A.CallTo(() => userResolver.FindByIdOrEmailAsync(A<string>.Ignored))
.MustNotHaveHappened();
}
private void MustNotSendEmail()
{
A.CallTo(() => emailSender.SendNewUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
A.CallTo(() => emailSender.SendExistingUserEmailAsync(A<IUser>.Ignored, A<IUser>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
private Envelope<IEvent> CreateEvent(string assignerType, bool isNewUser, bool isNewContributor = true, Instant? instant = null, int streamNumber = 2)
{
var @event = new AppContributorAssigned
{ {
Actor = new RefToken(assignerType, assignerId), Actor = new RefToken(assignerType, assignerId),
AppId = new NamedId<Guid>(Guid.NewGuid(), appName), AppId = new NamedId<Guid>(Guid.NewGuid(), appName),
ContributorId = assigneeId, ContributorId = assigneeId,
IsCreated = isNew IsCreated = isNewUser,
IsNew = isNewContributor,
}; };
var envelope = Envelope.Create(@event);
envelope.SetTimestamp(instant ?? SystemClock.Instance.GetCurrentInstant());
envelope.SetEventStreamNumber(streamNumber);
return envelope;
} }
} }
} }

Loading…
Cancel
Save