Преглед изворни кода

Functionality to update a password

Robert Marshall пре 3 година
родитељ
комит
5b4a86cc79

+ 57 - 0
src/Website.Tests/Controllers/AccountControllerTests.cs

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc.Routing;
 using Microsoft.AspNetCore.Mvc.ViewFeatures;
 using NSubstitute;
 using NSubstitute.ExceptionExtensions;
+using System.Security.Claims;
 using Website.Controllers;
 using Website.Data;
 using Website.Models.Auth;
@@ -146,5 +147,61 @@ namespace Website.Tests.Controllers {
 			result.Should().BeOfType<ViewResult>();
 			(result as ViewResult)?.Model.Should().BeEquivalentTo(expected);
 		}
+
+		[Fact]
+		public async Task UpdatePassword_WithConfirmPasswordNotMatching_RedirectsToIndexWithFailure() {
+			var authenticationProvider = Substitute.For<IAuthenticationProvider>();
+			var claimsPrincipal = Substitute.For<ClaimsPrincipal>();
+			claimsPrincipal.FindFirst(ClaimTypes.Name).Returns(new Claim(ClaimTypes.Name, "valid"));
+			var controller = new AccountController(authenticationProvider) {
+				ControllerContext = new ControllerContext {HttpContext = new DefaultHttpContext {User = claimsPrincipal}},
+				TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>()) // TempData needs to be set up in a unit test
+			};
+
+			var request = new UpdatePasswordRequest {Username = "valid", OldPassword = "correct", NewPassword = "new", ConfirmPassword = "no match"};
+
+			var result = await controller.UpdatePassword(request);
+			result.Should().BeOfType<RedirectToActionResult>();
+			(result as RedirectToActionResult).ActionName.Should().Be("Index");
+			controller.TempData["updatePassword"].Should().Be(false);
+		}
+
+		[Fact]
+		public async Task UpdatePassword_WithValidCredentials_RedirectsToIndexWithSuccess() {
+			var authenticationProvider = Substitute.For<IAuthenticationProvider>();
+			var claimsPrincipal = Substitute.For<ClaimsPrincipal>();
+			claimsPrincipal.FindFirst(ClaimTypes.Name).Returns(new Claim(ClaimTypes.Name, "valid"));
+			var controller = new AccountController(authenticationProvider) {
+				ControllerContext = new ControllerContext {HttpContext = new DefaultHttpContext {User = claimsPrincipal}},
+				TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>()) // TempData needs to be set up in a unit test
+			};
+
+			var request = new UpdatePasswordRequest {Username = "valid", OldPassword = "correct", NewPassword = "new", ConfirmPassword = "new"};
+			authenticationProvider.UpdateUserPassword(request).Returns(true);
+
+			var result = await controller.UpdatePassword(request);
+			result.Should().BeOfType<RedirectToActionResult>();
+			(result as RedirectToActionResult).ActionName.Should().Be("Index");
+			controller.TempData["updatePassword"].Should().Be(true);
+		}
+
+		[Fact]
+		public async Task UpdatePassword_WithInvalidCredentials_RedirectsToIndexWithFailure() {
+			var authenticationProvider = Substitute.For<IAuthenticationProvider>();
+			var claimsPrincipal = Substitute.For<ClaimsPrincipal>();
+			claimsPrincipal.FindFirst(ClaimTypes.Name).Returns(new Claim(ClaimTypes.Name, "valid"));
+			var controller = new AccountController(authenticationProvider) {
+				ControllerContext = new ControllerContext {HttpContext = new DefaultHttpContext {User = claimsPrincipal}},
+				TempData = new TempDataDictionary(new DefaultHttpContext(), Substitute.For<ITempDataProvider>()) // TempData needs to be set up in a unit test
+			};
+
+			var request = new UpdatePasswordRequest {Username = "valid", OldPassword = "incorrect", NewPassword = "new", ConfirmPassword = "new"};
+			authenticationProvider.UpdateUserPassword(request).Returns(false);
+
+			var result = await controller.UpdatePassword(request);
+			result.Should().BeOfType<RedirectToActionResult>();
+			(result as RedirectToActionResult).ActionName.Should().Be("Index");
+			controller.TempData["updatePassword"].Should().Be(false);
+		}
 	}
 }

+ 42 - 0
src/Website.Tests/Data/AuthenticationProviderTests.cs

@@ -58,5 +58,47 @@ namespace Website.Tests.Data {
 			var provider = new AuthenticationProvider(httpClient, cache, new CacheDurations());
 			(await provider.Authenticate(request)).Should().BeNull();
 		}
+
+		[Fact]
+		public async Task UpdateUserPassword_WithSuccessfulRequest_ReturnsTrue() {
+			var httpClient = new HttpClientBuilder()
+							 .WithMethod(HttpMethod.Post)
+							 .WithUrl("/user/updateuserpassword")
+							 .WithPostBody(@"{""Username"":""username"",""OldPassword"":""correct"",""NewPassword"":""new""}")
+							 .WithResponseCode(HttpStatusCode.NoContent)
+							 .Build();
+
+			var cache = Substitute.For<IMemoryCache>();
+
+			var request = new UpdatePasswordRequest {
+				Username = "username",
+				OldPassword = "correct",
+				NewPassword = "new"
+			};
+
+			var provider = new AuthenticationProvider(httpClient, cache, new CacheDurations());
+			(await provider.UpdateUserPassword(request)).Should().BeTrue();
+		}
+
+		[Fact]
+		public async Task UpdateUserPassword_WithUnsuccessfulRequest_ReturnsFalse() {
+			var httpClient = new HttpClientBuilder()
+							 .WithMethod(HttpMethod.Post)
+							 .WithUrl("/user/updateuserpassword")
+							 .WithPostBody(@"{""Username"":""username"",""OldPassword"":""incorrect"",""NewPassword"":""new""}")
+							 .WithResponseCode(HttpStatusCode.BadRequest)
+							 .Build();
+
+			var cache = Substitute.For<IMemoryCache>();
+
+			var request = new UpdatePasswordRequest {
+				Username = "username",
+				OldPassword = "incorrect",
+				NewPassword = "new"
+			};
+
+			var provider = new AuthenticationProvider(httpClient, cache, new CacheDurations());
+			(await provider.UpdateUserPassword(request)).Should().BeTrue();
+		}
 	}
 }

+ 2 - 0
src/Website.Tests/Data/BlogApiTests.cs

@@ -247,6 +247,7 @@ namespace Website.Tests.Data {
 			                 .WithMethod(HttpMethod.Post)
 			                 .WithUrl("/deletepost")
 			                 .WithPostBody("1")
+			                 .WithResponse("")
 			                 .Build(out var mockHttpMessageHandler);
 
 			var cache = Substitute.For<IMemoryCache>();
@@ -262,6 +263,7 @@ namespace Website.Tests.Data {
 			                 .WithMethod(HttpMethod.Post)
 			                 .WithUrl("/publishpost")
 			                 .WithPostBody("1")
+			                 .WithResponse("")
 			                 .Build(out var mockHttpMessageHandler);
 
 			var cache = Substitute.For<IMemoryCache>();

+ 12 - 1
src/Website.Tests/HttpClientBuilder.cs

@@ -11,6 +11,7 @@ namespace Website.Tests {
 		private HttpMethod _method;
 		private readonly Dictionary<string, string> _queries = new Dictionary<string, string>();
 		private HttpStatusCode _fallbackCode = HttpStatusCode.OK;
+		private HttpStatusCode _responseCode = HttpStatusCode.OK;
 
 		public HttpClientBuilder WithUrl(string url) {
 			_url = url;
@@ -42,12 +43,22 @@ namespace Website.Tests {
 			return this;
 		}
 
+		public HttpClientBuilder WithResponseCode(HttpStatusCode statusCode) {
+			_responseCode = statusCode;
+			return this;
+		}
+
 		public HttpClient Build(out MockHttpMessageHandler mockHttpMessageHandler) {
 			mockHttpMessageHandler = new MockHttpMessageHandler();
 
 			mockHttpMessageHandler.Fallback.Respond(_fallbackCode, message => message.Content = new StringContent(string.Empty));
 
-			var mockedRequest = mockHttpMessageHandler.Expect(_method, _url).Respond("application/json", _response ?? string.Empty);
+			var mockedRequest = mockHttpMessageHandler.Expect(_method, _url);
+
+			if (_response != null)
+				mockedRequest.Respond("application/json", _response ?? string.Empty);
+			else
+				mockedRequest.Respond(_responseCode);
 
 			if (_queries.Any())
 				mockedRequest.WithExactQueryString(_queries);

+ 7 - 0
src/Website/Controllers/AccountController.cs

@@ -61,5 +61,12 @@ namespace Website.Controllers {
 			await HttpContext.SignOutAsync();
 			return RedirectToAction(nameof(Login));
 		}
+
+		[HttpPost(nameof(UpdatePassword))]
+		public async Task<IActionResult> UpdatePassword(UpdatePasswordRequest request) {
+			request.Username = HttpContext.User.FindFirst(ClaimTypes.Name).Value;
+			TempData["updatePassword"] = request.NewPassword == request.ConfirmPassword && await _authenticationProvider.UpdateUserPassword(request);
+			return RedirectToAction(nameof(Index));
+		}
 	}
 }

+ 10 - 0
src/Website/Data/AuthenticationProvider.cs

@@ -17,5 +17,15 @@ namespace Website.Data {
 				return null;
 			}
 		}
+
+		public async Task<bool> UpdateUserPassword(UpdatePasswordRequest request) {
+			try {
+				await Patch<object>("user/updatepassword", null, new {request.Username, request.OldPassword, request.NewPassword});
+				return true;
+			}
+			catch (ApiCallException) {
+				return false;
+			}
+		}
 	}
 }

+ 1 - 0
src/Website/Data/IAuthenticationProvider.cs

@@ -4,5 +4,6 @@ using Website.Models.Auth;
 namespace Website.Data {
 	public interface IAuthenticationProvider {
 		Task<User> Authenticate(LoginRequest request);
+		Task<bool> UpdateUserPassword(UpdatePasswordRequest request);
 	}
 }

+ 8 - 0
src/Website/Models/Auth/UpdatePasswordRequest.cs

@@ -0,0 +1,8 @@
+namespace Website.Models.Auth {
+	public class UpdatePasswordRequest {
+		public string Username { get; set; }
+		public string OldPassword { get; set; }
+		public string NewPassword { get; set; }
+		public string ConfirmPassword { get; set; }
+	}
+}

+ 14 - 2
src/Website/Views/Account/Index.cshtml

@@ -1,4 +1,4 @@
-@model AccountViewModel
+@model AccountViewModel
 
 @{
 	ViewBag.Title = "Account";
@@ -7,9 +7,21 @@
 
 <a asp-action="Logout">Logout</a>
 
+<h2>Admin</h2>
 <div>
 	<a asp-controller="Admin" asp-action="ApiKeys">Manage API Keys</a>
 </div>
 <div>
 	<a asp-controller="Admin" asp-action="Mailboxes">Manage Mailboxes</a>
-</div>
+</div>
+
+<h2>Change password</h2>
+@if (TempData.ContainsKey("updatePassword")) {
+	<p>@((bool) TempData["updatePassword"] ? "Password updated" : "Update failed")</p>
+}
+<form asp-action="UpdatePassword">
+	<label class="block">Old password: <input type="password" name="oldPassword"></label>
+	<label class="block">New password: <input type="password" name="newPassword"></label>
+	<label class="block">Confirm password: <input type="password" name="confirmPassword"></label>
+	<button type="submit">Change</button>
+</form>