Skip to content

Commit 713a0f2

Browse files
iammukeshmjarvis
andauthored
test(identity): add handler tests for auth-critical flows (#1204)
* Add comprehensive handler tests for Identity module - Added GenerateTokenCommandHandler tests (login flow) - Added RefreshTokenCommandHandler tests (token refresh) - Added RegisterUserCommandHandler tests (user registration) - Added RevokeSessionCommandHandler tests (session management) - Added ChangePasswordCommandHandler tests (password change) Tests include: - Happy path scenarios - Invalid input handling (null checks) - Exception scenarios - CancellationToken propagation - Edge cases and error conditions Following existing test patterns from Multitenancy module: - Uses xUnit, Shouldly, NSubstitute, AutoFixture - Arrange/Act/Assert pattern - Comprehensive test coverage per handler Addresses issue #1173 * fix: add missing NSubstitute.ExceptionExtensions using directives - Added using NSubstitute.ExceptionExtensions to: - RevokeSessionCommandHandlerTests.cs - ChangePasswordCommandHandlerTests.cs - GenerateTokenCommandHandlerTests.cs - RegisterUserCommandHandlerTests.cs - Fixed IIntegrationEvent type in outboxStore assertions * fix: resolve all compiler warnings (56 → 0) - Make ConfigureRecipients and CreateBasicClaims static (CA1822) - Cache JsonSerializerOptions in VersionCommand (CA1869) - Delete unused TemplateEngineTests.cs debug file - Add NoWarn for false-positive warnings in CLI project: - CA1812: Classes instantiated via DI - CA1031: Intentional catch-all in TryLoad/IsValid methods - CA1303: CLI is not localized - CA1308: ToLowerInvariant intentional for identifiers - Other code style warnings that don't apply to CLI tools --------- Co-authored-by: jarvis <jarvis@codewithmukesh.com>
1 parent db28636 commit 713a0f2

File tree

10 files changed

+1497
-89
lines changed

10 files changed

+1497
-89
lines changed

src/BuildingBlocks/Mailing/Services/SmtpMailService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private void ConfigureSender(MimeMessage email, MailRequest request)
4646
email.Sender = new MailboxAddress(request.DisplayName ?? _settings.DisplayName, request.From ?? _settings.From);
4747
}
4848

49-
private void ConfigureRecipients(MimeMessage email, MailRequest request)
49+
private static void ConfigureRecipients(MimeMessage email, MailRequest request)
5050
{
5151
foreach (string address in request.To)
5252
{

src/Modules/Identity/Modules.Identity/Services/IdentityService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ private async Task<List<Claim>> BuildUserClaimsAsync(FshUser user, string tenant
177177
return claims;
178178
}
179179

180-
private List<Claim> CreateBasicClaims(FshUser user, string tenantId) =>
180+
private static List<Claim> CreateBasicClaims(FshUser user, string tenantId) =>
181181
[
182182
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
183183
new(ClaimTypes.NameIdentifier, user.Id),
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
using AutoFixture;
2+
using FSH.Framework.Core.Context;
3+
using FSH.Modules.Identity.Contracts.Services;
4+
using FSH.Modules.Identity.Contracts.v1.Users.ChangePassword;
5+
using FSH.Modules.Identity.Features.v1.Users.ChangePassword;
6+
using NSubstitute;
7+
using NSubstitute.ExceptionExtensions;
8+
9+
namespace Identity.Tests.Handlers;
10+
11+
/// <summary>
12+
/// Tests for ChangePasswordCommandHandler - handles password change operations.
13+
/// </summary>
14+
public sealed class ChangePasswordCommandHandlerTests
15+
{
16+
private readonly IUserService _userService;
17+
private readonly ICurrentUser _currentUser;
18+
private readonly ChangePasswordCommandHandler _sut;
19+
private readonly IFixture _fixture;
20+
21+
public ChangePasswordCommandHandlerTests()
22+
{
23+
_userService = Substitute.For<IUserService>();
24+
_currentUser = Substitute.For<ICurrentUser>();
25+
_sut = new ChangePasswordCommandHandler(_userService, _currentUser);
26+
_fixture = new Fixture();
27+
}
28+
29+
#region Handle - Happy Path Tests
30+
31+
[Fact]
32+
public async Task Handle_Should_ReturnSuccessMessage_When_PasswordIsChangedSuccessfully()
33+
{
34+
// Arrange
35+
var command = new ChangePasswordCommand
36+
{
37+
Password = "CurrentPassword123!",
38+
NewPassword = "NewPassword456!",
39+
ConfirmNewPassword = "NewPassword456!"
40+
};
41+
42+
var userId = _fixture.Create<Guid>();
43+
44+
_currentUser.IsAuthenticated().Returns(true);
45+
_currentUser.GetUserId().Returns(userId);
46+
47+
_userService.ChangePasswordAsync(command.Password, command.NewPassword, command.ConfirmNewPassword, userId.ToString())
48+
.Returns(Task.CompletedTask);
49+
50+
// Act
51+
var result = await _sut.Handle(command, CancellationToken.None);
52+
53+
// Assert
54+
result.ShouldBe("password reset email sent");
55+
}
56+
57+
[Fact]
58+
public async Task Handle_Should_CallUserServiceWithCorrectParameters_When_PasswordChangeIsRequested()
59+
{
60+
// Arrange
61+
var command = new ChangePasswordCommand
62+
{
63+
Password = "OldPassword123!",
64+
NewPassword = "NewPassword456!",
65+
ConfirmNewPassword = "NewPassword456!"
66+
};
67+
68+
var userId = _fixture.Create<Guid>();
69+
70+
_currentUser.IsAuthenticated().Returns(true);
71+
_currentUser.GetUserId().Returns(userId);
72+
73+
_userService.ChangePasswordAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
74+
.Returns(Task.CompletedTask);
75+
76+
// Act
77+
await _sut.Handle(command, CancellationToken.None);
78+
79+
// Assert
80+
await _userService.Received(1).ChangePasswordAsync(
81+
command.Password,
82+
command.NewPassword,
83+
command.ConfirmNewPassword,
84+
userId.ToString());
85+
}
86+
87+
#endregion
88+
89+
#region Handle - Authentication Tests
90+
91+
[Fact]
92+
public async Task Handle_Should_ThrowInvalidOperationException_When_UserIsNotAuthenticated()
93+
{
94+
// Arrange
95+
var command = new ChangePasswordCommand
96+
{
97+
Password = "CurrentPassword123!",
98+
NewPassword = "NewPassword456!",
99+
ConfirmNewPassword = "NewPassword456!"
100+
};
101+
102+
_currentUser.IsAuthenticated().Returns(false);
103+
104+
// Act & Assert
105+
var exception = await Should.ThrowAsync<InvalidOperationException>(
106+
async () => await _sut.Handle(command, CancellationToken.None));
107+
108+
exception.Message.ShouldBe("User is not authenticated.");
109+
}
110+
111+
[Fact]
112+
public async Task Handle_Should_CheckAuthentication_BeforeGettingUserId()
113+
{
114+
// Arrange
115+
var command = new ChangePasswordCommand
116+
{
117+
Password = "CurrentPassword123!",
118+
NewPassword = "NewPassword456!",
119+
ConfirmNewPassword = "NewPassword456!"
120+
};
121+
122+
_currentUser.IsAuthenticated().Returns(false);
123+
124+
// Act
125+
await Should.ThrowAsync<InvalidOperationException>(
126+
async () => await _sut.Handle(command, CancellationToken.None));
127+
128+
// Assert
129+
_currentUser.Received(1).IsAuthenticated();
130+
_currentUser.DidNotReceive().GetUserId();
131+
}
132+
133+
#endregion
134+
135+
#region Handle - Current User Tests
136+
137+
[Fact]
138+
public async Task Handle_Should_GetUserIdFromCurrentUser_When_UserIsAuthenticated()
139+
{
140+
// Arrange
141+
var command = new ChangePasswordCommand
142+
{
143+
Password = "CurrentPassword123!",
144+
NewPassword = "NewPassword456!",
145+
ConfirmNewPassword = "NewPassword456!"
146+
};
147+
148+
var userId = _fixture.Create<Guid>();
149+
150+
_currentUser.IsAuthenticated().Returns(true);
151+
_currentUser.GetUserId().Returns(userId);
152+
153+
_userService.ChangePasswordAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
154+
.Returns(Task.CompletedTask);
155+
156+
// Act
157+
await _sut.Handle(command, CancellationToken.None);
158+
159+
// Assert
160+
_currentUser.Received(1).IsAuthenticated();
161+
_currentUser.Received(1).GetUserId();
162+
}
163+
164+
[Fact]
165+
public async Task Handle_Should_ConvertUserIdToString_When_PassingToUserService()
166+
{
167+
// Arrange
168+
var command = new ChangePasswordCommand
169+
{
170+
Password = "CurrentPassword123!",
171+
NewPassword = "NewPassword456!",
172+
ConfirmNewPassword = "NewPassword456!"
173+
};
174+
175+
var userId = _fixture.Create<Guid>();
176+
177+
_currentUser.IsAuthenticated().Returns(true);
178+
_currentUser.GetUserId().Returns(userId);
179+
180+
_userService.ChangePasswordAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
181+
.Returns(Task.CompletedTask);
182+
183+
// Act
184+
await _sut.Handle(command, CancellationToken.None);
185+
186+
// Assert
187+
await _userService.Received(1).ChangePasswordAsync(
188+
command.Password,
189+
command.NewPassword,
190+
command.ConfirmNewPassword,
191+
userId.ToString());
192+
}
193+
194+
#endregion
195+
196+
#region Handle - Exception Tests
197+
198+
[Fact]
199+
public async Task Handle_Should_ThrowException_When_UserServiceThrows()
200+
{
201+
// Arrange
202+
var command = new ChangePasswordCommand
203+
{
204+
Password = "WrongPassword123!",
205+
NewPassword = "NewPassword456!",
206+
ConfirmNewPassword = "NewPassword456!"
207+
};
208+
209+
var userId = _fixture.Create<Guid>();
210+
211+
_currentUser.IsAuthenticated().Returns(true);
212+
_currentUser.GetUserId().Returns(userId);
213+
214+
var expectedExceptionMessage = "Current password is incorrect";
215+
_userService.ChangePasswordAsync(command.Password, command.NewPassword, command.ConfirmNewPassword, userId.ToString())
216+
.ThrowsAsync(new UnauthorizedAccessException(expectedExceptionMessage));
217+
218+
// Act & Assert
219+
var exception = await Should.ThrowAsync<UnauthorizedAccessException>(
220+
async () => await _sut.Handle(command, CancellationToken.None));
221+
222+
exception.Message.ShouldBe(expectedExceptionMessage);
223+
}
224+
225+
[Fact]
226+
public async Task Handle_Should_ThrowException_When_GetUserIdThrows()
227+
{
228+
// Arrange
229+
var command = new ChangePasswordCommand
230+
{
231+
Password = "CurrentPassword123!",
232+
NewPassword = "NewPassword456!",
233+
ConfirmNewPassword = "NewPassword456!"
234+
};
235+
236+
_currentUser.IsAuthenticated().Returns(true);
237+
_currentUser.GetUserId().Throws(new InvalidOperationException("User ID not available"));
238+
239+
// Act & Assert
240+
await Should.ThrowAsync<InvalidOperationException>(
241+
async () => await _sut.Handle(command, CancellationToken.None));
242+
}
243+
244+
#endregion
245+
246+
#region Handle - Null Command Tests
247+
248+
[Fact]
249+
public async Task Handle_Should_ThrowArgumentNullException_When_CommandIsNull()
250+
{
251+
// Act & Assert
252+
await Should.ThrowAsync<ArgumentNullException>(async () =>
253+
await _sut.Handle(null!, CancellationToken.None));
254+
}
255+
256+
#endregion
257+
258+
#region Handle - CancellationToken Tests
259+
260+
[Fact]
261+
public async Task Handle_Should_HandleCancellationToken_Properly()
262+
{
263+
// Arrange
264+
var command = new ChangePasswordCommand
265+
{
266+
Password = "CurrentPassword123!",
267+
NewPassword = "NewPassword456!",
268+
ConfirmNewPassword = "NewPassword456!"
269+
};
270+
271+
var userId = _fixture.Create<Guid>();
272+
using var cts = new CancellationTokenSource();
273+
var cancellationToken = cts.Token;
274+
275+
_currentUser.IsAuthenticated().Returns(true);
276+
_currentUser.GetUserId().Returns(userId);
277+
278+
_userService.ChangePasswordAsync(command.Password, command.NewPassword, command.ConfirmNewPassword, userId.ToString())
279+
.Returns(Task.CompletedTask);
280+
281+
// Act
282+
var result = await _sut.Handle(command, cancellationToken);
283+
284+
// Assert
285+
result.ShouldBe("password reset email sent");
286+
}
287+
288+
#endregion
289+
290+
#region Handle - Edge Cases
291+
292+
[Fact]
293+
public async Task Handle_Should_HandleEmptyPasswords_When_ProvidedInCommand()
294+
{
295+
// Arrange
296+
var command = new ChangePasswordCommand
297+
{
298+
Password = "",
299+
NewPassword = "",
300+
ConfirmNewPassword = ""
301+
};
302+
303+
var userId = _fixture.Create<Guid>();
304+
305+
_currentUser.IsAuthenticated().Returns(true);
306+
_currentUser.GetUserId().Returns(userId);
307+
308+
_userService.ChangePasswordAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
309+
.Returns(Task.CompletedTask);
310+
311+
// Act
312+
var result = await _sut.Handle(command, CancellationToken.None);
313+
314+
// Assert
315+
result.ShouldBe("password reset email sent");
316+
await _userService.Received(1).ChangePasswordAsync("", "", "", userId.ToString());
317+
}
318+
319+
#endregion
320+
}

0 commit comments

Comments
 (0)