From 8f3ea03d75f55b6765a89b4624809089ed14ed1f Mon Sep 17 00:00:00 2001 From: Robert Marshall Date: Sun, 6 Jun 2021 09:34:05 +0100 Subject: [PATCH] Functionality to update a password --- .../Controllers/AuthControllerTests.cs | 33 ++++++++++++++ .../Controllers/UserController.cs | 6 +++ .../Users/AuthenticatorTests.cs | 43 +++++++++++++++++++ src/Robware.Auth/Users/Authenticator.cs | 14 ++++++ src/Robware.Auth/Users/IAuthenticator.cs | 1 + src/Robware.Auth/Users/IUsers.cs | 1 + src/Robware.Data/UserRepository.cs | 7 ++- 7 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/Robware.Api.Auth.Tests/Controllers/AuthControllerTests.cs b/src/Robware.Api.Auth.Tests/Controllers/AuthControllerTests.cs index b00d79e..e37592a 100644 --- a/src/Robware.Api.Auth.Tests/Controllers/AuthControllerTests.cs +++ b/src/Robware.Api.Auth.Tests/Controllers/AuthControllerTests.cs @@ -63,5 +63,38 @@ namespace Robware.Api.Auth.Tests.Controllers { var controller = new UserController(logger, authenticator); (await controller.Authenticate(request)).Result.Should().BeOfType(); } + + [Fact] + public async Task UpdatePassword_WithExistingUserAndCorrectPassword_Returns204() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + var controller = new UserController(logger, authenticator); + + authenticator.UpdateUserPassword("valid", "correct", "new").Returns(AuthenticationResult.Success); + + (await controller.UpdatePassword("valid", "correct", "new")).Should().BeOfType(); + } + + [Fact] + public async Task UpdatePassword_WithExistingUserAndIncorrectPassword_Returns400() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + var controller = new UserController(logger, authenticator); + + authenticator.UpdateUserPassword("valid", "incorrect", "new").Returns(AuthenticationResult.IncorrectPassword); + + (await controller.UpdatePassword("valid", "incorrect", "new")).Should().BeOfType(); + } + + [Fact] + public async Task UpdatePassword_WithNonExistingUser_Returns400() { + var logger = Substitute.For>(); + var authenticator = Substitute.For(); + var controller = new UserController(logger, authenticator); + + authenticator.UpdateUserPassword("invalid", "incorrect", "new").Returns(AuthenticationResult.NotFound); + + (await controller.UpdatePassword("invalid", "incorrect", "new")).Should().BeOfType(); + } } } diff --git a/src/Robware.Api.Auth/Controllers/UserController.cs b/src/Robware.Api.Auth/Controllers/UserController.cs index 0cbe14a..c0440e7 100644 --- a/src/Robware.Api.Auth/Controllers/UserController.cs +++ b/src/Robware.Api.Auth/Controllers/UserController.cs @@ -31,5 +31,11 @@ namespace Robware.Api.Auth.Controllers { throw new ArgumentOutOfRangeException(); } } + + [HttpPatch(nameof(UpdatePassword))] + public async Task UpdatePassword(string username, string oldPassword, string newPassword) { + var result = await _authenticator.UpdateUserPassword(username, oldPassword, newPassword); + return result == AuthenticationResult.Success ? (ActionResult)NoContent() : BadRequest(); + } } } diff --git a/src/Robware.Auth.Tests/Users/AuthenticatorTests.cs b/src/Robware.Auth.Tests/Users/AuthenticatorTests.cs index 4e8c05f..002dfdd 100644 --- a/src/Robware.Auth.Tests/Users/AuthenticatorTests.cs +++ b/src/Robware.Auth.Tests/Users/AuthenticatorTests.cs @@ -46,5 +46,48 @@ namespace Robware.Auth.Tests.Users { (await auth.Authenticate("test", "password")).Should().BeEquivalentTo((AuthenticationResult.NotFound, null as User)); } + + [Fact] + public async Task UpdateUserPassword_WithExistingUserAndCorrectPassword_ReturnsSuccess() { + var users = Substitute.For(); + var crypto = Substitute.For(); + crypto.Encrypt("correct").Returns("correct"); + crypto.Encrypt("new").Returns("encrypted"); + + var user = new TestUser("valid", "correct"); + users.GetByEmail("valid").Returns(user); + users.UpdateUserPassword("valid", "encrypted").Returns(true); + + var auth = new Authenticator(users, crypto); + (await auth.UpdateUserPassword("valid", "correct", "new")).Should().Be(AuthenticationResult.Success); + await users.Received(1).UpdateUserPassword("valid", "encrypted"); + } + + [Fact] + public async Task UpdateUserPassword_WithExistingUserAndIncorrectPassword_ReturnsIncorrectPassword() { + var users = Substitute.For(); + var crypto = Substitute.For(); + crypto.Encrypt("correct").Returns("correct"); + + var user = new TestUser("valid", "correct"); + users.GetByEmail("valid").Returns(user); + users.UpdateUserPassword("valid", "encrypted").Returns(true); + + var auth = new Authenticator(users, crypto); + (await auth.UpdateUserPassword("valid", "incorrect", "new")).Should().Be(AuthenticationResult.IncorrectPassword); + await users.Received(0).UpdateUserPassword("valid", "encrypted"); + } + + [Fact] + public async Task UpdateUserPassword_WithNonExistentUser_ReturnsNotFound() { + var users = Substitute.For(); + var crypto = Substitute.For(); + + users.GetByEmail("invalid").Throws(new UserNotFoundException("")); + + var auth = new Authenticator(users, crypto); + (await auth.UpdateUserPassword("invalid", "correct", "new")).Should().Be(AuthenticationResult.NotFound); + await users.Received(0).UpdateUserPassword("valid", "encrypted"); + } } } \ No newline at end of file diff --git a/src/Robware.Auth/Users/Authenticator.cs b/src/Robware.Auth/Users/Authenticator.cs index 52f22d0..953ebb5 100644 --- a/src/Robware.Auth/Users/Authenticator.cs +++ b/src/Robware.Auth/Users/Authenticator.cs @@ -19,6 +19,20 @@ namespace Robware.Auth.Users { return (AuthenticationResult.NotFound, null); } } + + public async Task UpdateUserPassword(string username, string oldPassword, string newPassword) { + try { + var user = await _users.GetByEmail(username); + if (_crypto.Encrypt(oldPassword) != user.Password) + return AuthenticationResult.IncorrectPassword; + + await _users.UpdateUserPassword(username, _crypto.Encrypt(newPassword)); + return AuthenticationResult.Success; + } + catch (UserNotFoundException) { + return AuthenticationResult.NotFound; + } + } } public enum AuthenticationResult { diff --git a/src/Robware.Auth/Users/IAuthenticator.cs b/src/Robware.Auth/Users/IAuthenticator.cs index 1913957..04cf0d6 100644 --- a/src/Robware.Auth/Users/IAuthenticator.cs +++ b/src/Robware.Auth/Users/IAuthenticator.cs @@ -3,5 +3,6 @@ namespace Robware.Auth.Users { public interface IAuthenticator { Task<(AuthenticationResult Result, User User)> Authenticate(string username, string password); + Task UpdateUserPassword(string username, string oldPassword, string newPassword); } } \ No newline at end of file diff --git a/src/Robware.Auth/Users/IUsers.cs b/src/Robware.Auth/Users/IUsers.cs index e2d7ae5..e229b59 100644 --- a/src/Robware.Auth/Users/IUsers.cs +++ b/src/Robware.Auth/Users/IUsers.cs @@ -3,5 +3,6 @@ namespace Robware.Auth.Users { public interface IUsers { Task GetByEmail(string email); + Task UpdateUserPassword(string email, string newPassword); } } \ No newline at end of file diff --git a/src/Robware.Data/UserRepository.cs b/src/Robware.Data/UserRepository.cs index 7539a1d..e91588c 100644 --- a/src/Robware.Data/UserRepository.cs +++ b/src/Robware.Data/UserRepository.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Robware.Data { public class UserRepository : IUsers { - IMongoCollection _collection; + private readonly IMongoCollection _collection; public UserRepository(IMongoDatabase database) { _collection = database.GetCollection("users"); @@ -19,5 +19,10 @@ namespace Robware.Data { return new DatabaseUser(result); } + + public async Task UpdateUserPassword(string email, string newPassword) { + var result = (await _collection.UpdateOneAsync(state => state.Email == email, Builders.Update.Set(state => state.Password, newPassword))); + return result.ModifiedCount == 1; + } } }