Skip to content

Commit 751f9dd

Browse files
author
gauffininteractive
committed
* Correcting issues regarding invites for already invited users
* The dashboard are now updated accordingly when the user is not member of any applications * the typescript compilation issues should now have been fixed so that no merge issues regarding sourceMap/line endings should occur.
1 parent 277af9e commit 751f9dd

File tree

54 files changed

+5556
-4995
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5556
-4995
lines changed

src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitation.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.ComponentModel.DataAnnotations;
23
using DotNetCqs;
34

45
namespace OneTrueError.Api.Core.Accounts.Requests
@@ -62,6 +63,7 @@ protected AcceptInvitation()
6263
/// sure that this one is assigned to the created account.
6364
/// </para>
6465
/// </remarks>
66+
[Required]
6567
public string AcceptedEmail { get; set; }
6668

6769
/// <summary>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using DotNetCqs;
4+
using FluentAssertions;
5+
using NSubstitute;
6+
using OneTrueError.Api.Core.Applications.Events;
7+
using OneTrueError.Api.Core.Invitations.Commands;
8+
using OneTrueError.Api.Core.Messaging.Commands;
9+
using OneTrueError.App.Core.Applications;
10+
using OneTrueError.App.Core.Invitations;
11+
using OneTrueError.App.Core.Invitations.CommandHandlers;
12+
using OneTrueError.App.Core.Users;
13+
using OneTrueError.Infrastructure.Configuration;
14+
using Xunit;
15+
16+
namespace OneTrueError.App.Tests.Core.Applications.Commands
17+
{
18+
public class InviteUserHandlerTests
19+
{
20+
private readonly IApplicationRepository _applicationRepository;
21+
private readonly ICommandBus _commandBus;
22+
private readonly IEventBus _eventBus;
23+
private readonly IInvitationRepository _invitationRepository;
24+
private readonly InviteUserHandler _sut;
25+
private readonly IUserRepository _userRepository;
26+
27+
public InviteUserHandlerTests()
28+
{
29+
_invitationRepository = Substitute.For<IInvitationRepository>();
30+
_userRepository = Substitute.For<IUserRepository>();
31+
_applicationRepository = Substitute.For<IApplicationRepository>();
32+
_commandBus = Substitute.For<ICommandBus>();
33+
_eventBus = Substitute.For<IEventBus>();
34+
_userRepository.GetUserAsync(1).Returns(new User(1, "First"));
35+
_applicationRepository.GetByIdAsync(1).Returns(new Application(1, "MyApp"));
36+
ConfigurationStore.Instance = new TestStore();
37+
_sut = new InviteUserHandler(_invitationRepository, _eventBus, _userRepository, _applicationRepository,
38+
_commandBus);
39+
}
40+
41+
[Fact]
42+
public async Task should_create_an_invite_for_a_new_user()
43+
{
44+
var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1};
45+
var members = new[] {new ApplicationTeamMember(1, 3)};
46+
ApplicationTeamMember actual = null;
47+
_applicationRepository.GetTeamMembersAsync(1).Returns(members);
48+
_applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any<ApplicationTeamMember>()))
49+
.Do(x => actual = x.Arg<ApplicationTeamMember>());
50+
51+
await _sut.ExecuteAsync(cmd);
52+
53+
_applicationRepository.Received().CreateAsync(Arg.Any<ApplicationTeamMember>());
54+
actual.EmailAddress.Should().Be(cmd.EmailAddress);
55+
actual.ApplicationId.Should().Be(cmd.ApplicationId);
56+
actual.AddedAtUtc.Should().BeCloseTo(DateTime.UtcNow, 1000);
57+
actual.AddedByName.Should().Be("First");
58+
}
59+
60+
[Fact]
61+
public async Task should_not_allow_invites_when_the_invited_user_already_have_an_account()
62+
{
63+
var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 2};
64+
var members = new[] {new ApplicationTeamMember(1, 3)};
65+
_userRepository.FindByEmailAsync(cmd.EmailAddress).Returns(new User(3, "existing"));
66+
_applicationRepository.GetTeamMembersAsync(1).Returns(members);
67+
68+
await _sut.ExecuteAsync(cmd);
69+
70+
await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any<ApplicationTeamMember>());
71+
}
72+
73+
[Fact]
74+
public async Task should_not_allow_invites_when_the_invited_user_already_have_an_pending_invite()
75+
{
76+
var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1};
77+
var members = new[] {new ApplicationTeamMember(1, cmd.EmailAddress)};
78+
_applicationRepository.GetTeamMembersAsync(1).Returns(members);
79+
80+
await _sut.ExecuteAsync(cmd);
81+
82+
await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any<ApplicationTeamMember>());
83+
}
84+
85+
[Fact]
86+
public async Task should_notify_the_system_of_the_invite()
87+
{
88+
var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1};
89+
var members = new[] {new ApplicationTeamMember(1, 3)};
90+
ApplicationTeamMember actual = null;
91+
_applicationRepository.GetTeamMembersAsync(1).Returns(members);
92+
_applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any<ApplicationTeamMember>()))
93+
.Do(x => actual = x.Arg<ApplicationTeamMember>());
94+
95+
await _sut.ExecuteAsync(cmd);
96+
97+
_applicationRepository.Received().CreateAsync(Arg.Any<ApplicationTeamMember>());
98+
_eventBus.Received().PublishAsync(Arg.Any<UserInvitedToApplication>());
99+
}
100+
101+
[Fact]
102+
public async Task should_send_an_invitation_email()
103+
{
104+
var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1};
105+
var members = new[] {new ApplicationTeamMember(1, 3)};
106+
ApplicationTeamMember actual = null;
107+
_applicationRepository.GetTeamMembersAsync(1).Returns(members);
108+
_applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any<ApplicationTeamMember>()))
109+
.Do(x => actual = x.Arg<ApplicationTeamMember>());
110+
111+
await _sut.ExecuteAsync(cmd);
112+
113+
_commandBus.Received().ExecuteAsync(Arg.Any<SendEmail>());
114+
}
115+
}
116+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System.Threading.Tasks;
2+
using DotNetCqs;
3+
using FluentAssertions;
4+
using NSubstitute;
5+
using OneTrueError.Api.Core.Accounts.Events;
6+
using OneTrueError.Api.Core.Accounts.Requests;
7+
using OneTrueError.App.Core.Accounts;
8+
using OneTrueError.App.Core.Invitations;
9+
using OneTrueError.App.Core.Invitations.CommandHandlers;
10+
using Xunit;
11+
12+
namespace OneTrueError.App.Tests.Core.Invitations.Commands
13+
{
14+
public class AcceptInvitationHandlerTests
15+
{
16+
private IInvitationRepository _repository;
17+
private IAccountRepository _accountRepository;
18+
private IEventBus _eventBus;
19+
private AcceptInvitationHandler _sut;
20+
private Account _invitedAccount;
21+
private const int InvitedAccountId = 999;
22+
23+
24+
public AcceptInvitationHandlerTests()
25+
{
26+
_repository = Substitute.For<IInvitationRepository>();
27+
_accountRepository = Substitute.For<IAccountRepository>();
28+
_eventBus = Substitute.For<IEventBus>();
29+
_sut = new AcceptInvitationHandler(_repository, _accountRepository, _eventBus);
30+
_invitedAccount = new Account("arne", "1234");
31+
_invitedAccount.SetId(InvitedAccountId);
32+
_invitedAccount.SetVerifiedEmail("jonas@gauffin.com");
33+
_accountRepository.GetByIdAsync(InvitedAccountId).Returns(_invitedAccount);
34+
}
35+
36+
[Fact]
37+
public async Task should_delete_invitation_when_its_accepted_to_prevent_creating_multiple_accounts_with_the_same_invitation_key()
38+
{
39+
var invitation = new Invitation("invited@test.com", "inviter");
40+
var request = new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"};
41+
invitation.Add(1, "arne");
42+
_repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation);
43+
44+
var actual = await _sut.ExecuteAsync(request);
45+
46+
actual.Should().NotBeNull();
47+
}
48+
49+
[Fact]
50+
public async Task should_notify_system_of_the_accepted_invitation()
51+
{
52+
var invitation = new Invitation("invited@test.com", "inviter");
53+
var request = new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" };
54+
invitation.Add(1, "arne");
55+
_repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation);
56+
57+
var actual = await _sut.ExecuteAsync(request);
58+
59+
_eventBus.Received().PublishAsync(Arg.Any<InvitationAccepted>());
60+
var evt = _eventBus.Method("PublishAsync").Arg<InvitationAccepted>();
61+
evt.AcceptedEmailAddress.Should().Be(request.AcceptedEmail);
62+
evt.AccountId.Should().Be(InvitedAccountId);
63+
evt.ApplicationIds[0].Should().Be(1);
64+
evt.UserName.Should().Be(_invitedAccount.UserName);
65+
}
66+
67+
[Fact]
68+
public async Task should_create_an_Account_for_invites_to_new_users()
69+
{
70+
var invitation = new Invitation("invited@test.com", "inviter");
71+
var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" };
72+
invitation.Add(1, "arne");
73+
_repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation);
74+
_accountRepository
75+
.WhenForAnyArgs(x => x.CreateAsync(null))
76+
.Do(x => x.Arg<Account>().SetId(52));
77+
78+
79+
var actual = await _sut.ExecuteAsync(request);
80+
81+
_accountRepository.Received().CreateAsync(Arg.Any<Account>());
82+
var evt = _eventBus.Method("PublishAsync").Arg<InvitationAccepted>();
83+
evt.AccountId.Should().Be(52);
84+
}
85+
86+
[Fact]
87+
public async Task should_publish_AccountRegistered_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow()
88+
{
89+
var invitation = new Invitation("invited@test.com", "inviter");
90+
var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" };
91+
invitation.Add(1, "arne");
92+
_repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation);
93+
_accountRepository
94+
.WhenForAnyArgs(x => x.CreateAsync(null))
95+
.Do(x => x.Arg<Account>().SetId(52));
96+
97+
98+
await _sut.ExecuteAsync(request);
99+
100+
_eventBus.Received().PublishAsync(Arg.Any<AccountRegistered>());
101+
var evt = _eventBus.Method("PublishAsync").Arg<AccountRegistered>();
102+
evt.AccountId.Should().Be(52);
103+
}
104+
105+
[Fact]
106+
public async Task should_publish_AccountActivated_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow()
107+
{
108+
var invitation = new Invitation("invited@test.com", "inviter");
109+
var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" };
110+
invitation.Add(1, "arne");
111+
_repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation);
112+
_accountRepository
113+
.WhenForAnyArgs(x => x.CreateAsync(null))
114+
.Do(x => x.Arg<Account>().SetId(52));
115+
116+
117+
await _sut.ExecuteAsync(request);
118+
119+
_eventBus.Received().PublishAsync(Arg.Any<AccountActivated>());
120+
var evt = _eventBus.Method("PublishAsync").Arg<AccountActivated>();
121+
evt.AccountId.Should().Be(52);
122+
}
123+
124+
125+
[Fact]
126+
public async Task should_ignore_invitations_where_the_key_is_not_registered_in_the_db()
127+
{
128+
var request = new AcceptInvitation(InvitedAccountId, "invalid") { AcceptedEmail = "arne@gauffin.com" };
129+
130+
var actual = await _sut.ExecuteAsync(request);
131+
132+
actual.Should().BeNull();
133+
}
134+
}
135+
}

src/Server/OneTrueError.App.Tests/NSubstittueExtensions.cs

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using NSubstitute;
45
using NSubstitute.Core;
@@ -7,12 +8,17 @@ namespace OneTrueError.App.Tests
78
{
89
internal static class NSubstitueExtensions
910
{
10-
public static MethodWrapper Method(this object instance, string methodName)
11+
public static MethodListWrapper Method(this object instance, string methodName)
1112
{
1213
var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList();
13-
if (calls.Count != 1)
14-
throw new InvalidOperationException("More than one call to '" + methodName +
15-
"', use overload with invocation index.");
14+
return new MethodListWrapper(instance, calls);
15+
}
16+
17+
public static MethodWrapper Method(this object instance, string methodName, int indexer)
18+
{
19+
var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList();
20+
if (calls.Count <= indexer)
21+
throw new InvalidOperationException("There are only " + calls.Count + " calls available, you specified index " + indexer);
1622

1723
return new MethodWrapper(instance, calls[0]);
1824
}
@@ -31,7 +37,7 @@ public MethodWrapper(object instance, ICall call)
3137

3238
public TArgument Arg<TArgument>(int index)
3339
{
34-
if (index < 0 || index >= _call.GetArguments().Length)
40+
if ((index < 0) || (index >= _call.GetArguments().Length))
3541
throw new InvalidOperationException("Method '" + _call.GetMethodInfo().Name +
3642
"' do not have that many arguments.");
3743

@@ -52,4 +58,52 @@ public TArgument Arg<TArgument>()
5258
return (TArgument) args[0];
5359
}
5460
}
61+
62+
internal class MethodListWrapper
63+
{
64+
private readonly IList<ICall> _calls;
65+
private readonly object _instance;
66+
67+
public MethodListWrapper(object instance, IList<ICall> calls)
68+
{
69+
_instance = instance;
70+
_calls = calls;
71+
}
72+
73+
public TArgument Arg<TArgument>(int index)
74+
{
75+
List<object> values = new List<object>();
76+
foreach (var call in _calls)
77+
{
78+
if ((index < 0) || (index >= call.GetArguments().Length))
79+
continue;
80+
81+
if (call.GetArguments()[index] is TArgument)
82+
values.Add(call.GetArguments()[index]);
83+
}
84+
if (values.Count > 1)
85+
throw new InvalidOperationException("There was multiple calls to "+ _calls.First().GetMethodInfo().Name + " with an argument of type "+ typeof(TArgument) + ". Use method call indexer.");
86+
if (values.Count == 1)
87+
return (TArgument) values[0];
88+
89+
throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument));
90+
}
91+
92+
public TArgument Arg<TArgument>()
93+
{
94+
List<object> values = new List<object>();
95+
foreach (var call in _calls)
96+
{
97+
var arg = call.GetArguments().FirstOrDefault(x => x.GetType() == typeof(TArgument));
98+
if (arg != null)
99+
values.Add(arg);
100+
}
101+
if (values.Count > 1)
102+
throw new InvalidOperationException("There was multiple calls to " + _calls.First().GetMethodInfo().Name + " with an argument of type " + typeof(TArgument) + ". Use method call indexer.");
103+
if (values.Count == 1)
104+
return (TArgument) values[0];
105+
106+
throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument));
107+
}
108+
}
55109
}

src/Server/OneTrueError.App.Tests/OneTrueError.App.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ErrorReport>prompt</ErrorReport>
2727
<WarningLevel>4</WarningLevel>
2828
<CodeAnalysisRuleSet>ManagedMinimumRules.ruleset</CodeAnalysisRuleSet>
29+
<NoWarn>4014</NoWarn>
2930
</PropertyGroup>
3031
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
3132
<DebugType>pdbonly</DebugType>
@@ -103,6 +104,9 @@
103104
<Compile Include="Configuration\TestEntitites\WriteTestSection.cs" />
104105
<Compile Include="Core\Accounts\AccountTests.cs" />
105106
<Compile Include="Core\Accounts\CommandHandlers\RegisterAccountHandlerTests.cs" />
107+
<Compile Include="Core\Applications\Commands\InviteUserHandlerTests.cs" />
108+
<Compile Include="Core\Invitations\Commands\AcceptInvitationHandlerTests.cs" />
109+
<Compile Include="TestStore.cs" />
106110
<Compile Include="NSubstittueExtensions.cs" />
107111
<Compile Include="ObjectExtensions.cs" />
108112
<Compile Include="Properties\AssemblyInfo.cs" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using OneTrueError.App.Configuration;
3+
using OneTrueError.Infrastructure.Configuration;
4+
5+
namespace OneTrueError.App.Tests
6+
{
7+
public class TestStore : ConfigurationStore
8+
{
9+
public override T Load<T>()
10+
{
11+
if (typeof(T) == typeof(BaseConfiguration))
12+
return (T) (object) new BaseConfiguration {BaseUrl = new Uri("http://localhost/")};
13+
14+
return new T();
15+
}
16+
17+
public override void Store(IConfigurationSection section)
18+
{
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)