Skip to content

Commit f7e06a3

Browse files
Merge pull request #55 from blackjackkent/development
Deploy account deletion endpoint
2 parents b601078 + 4a399fa commit f7e06a3

File tree

8 files changed

+193
-5
lines changed

8 files changed

+193
-5
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ You can run all unit tests using your preferred C# test runner. To generate a co
3636

3737
This application communicates with the following external services:
3838

39-
1. A SQL Server database for Tracker-specific account information. The connection string for this database is set in `./RPThreadTrackerV3.BackEnd/appsettings.json`. You can use the scripts in `./SQL/InitDatabase` to set up a local copy of the database tables for development purposes.
39+
40+
1. A SQL Server database for Tracker-specific account information. The connection string for this database is set in `./RPThreadTrackerV3.BackEnd/appsettings.json`. You can use the following command to set up a local copy of the database tables for development purposes:
41+
42+
```
43+
cd ./SQL/InitDatabase
44+
sh ./InitDatabase.sh "(localdb)\mssqllocaldb" # or other local SQL server info
45+
```
4046
2. A DocumentDB NoSQL database for maintaining information about customized public views. You can run a local DocumentDB server using the [Azure CosmosDB Emulator](https://docs.microsoft.com/en-us/azure/cosmos-db/local-emulator), and set the connection information for this server in the `./RPThreadTrackerV3.BackEnd/appsettings.secure.json` file that you created earlier.
4147

4248
Unfortunately, this portion of the service can only be run on Windows machines at this time.

RPThreadTrackerV3.BackEnd.Test/Controllers/UserControllerTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,51 @@ public async Task ReturnsOkWhenRequestSuccessful()
171171
}
172172
}
173173

174+
public class DeleteAccount : UserControllerTests
175+
{
176+
[Fact]
177+
public async Task ReturnsServerErrorWhenUnexpectedErrorOccurs()
178+
{
179+
// Arrange
180+
_mockAuthService.Setup(s => s.DeleteAccount(It.IsAny<ClaimsPrincipal>(), _mockUserManager.Object))
181+
.Throws<NullReferenceException>();
182+
183+
// Act
184+
var result = await Controller.DeleteAccount();
185+
186+
// Assert
187+
result.Should().BeOfType<ObjectResult>();
188+
((ObjectResult)result).StatusCode.Should().Be(500);
189+
}
190+
191+
[Fact]
192+
public async Task ReturnsBadRequestWhenRequestIsInvalid()
193+
{
194+
// Arrange
195+
var exception = new InvalidAccountDeletionException(new List<string> { "error1", "error2" });
196+
_mockAuthService.Setup(s => s.DeleteAccount(It.IsAny<ClaimsPrincipal>(), _mockUserManager.Object))
197+
.Throws(exception);
198+
199+
// Act
200+
var result = await Controller.DeleteAccount();
201+
var body = ((BadRequestObjectResult)result).Value as List<string>;
202+
203+
// Assert
204+
result.Should().BeOfType<BadRequestObjectResult>();
205+
body.Should().HaveCount(1);
206+
}
207+
208+
[Fact]
209+
public async Task ReturnsOkWhenRequestSuccessful()
210+
{
211+
// Act
212+
var result = await Controller.DeleteAccount();
213+
214+
// Assert
215+
result.Should().BeOfType<OkResult>();
216+
}
217+
}
218+
174219
public class ChangeAccountInformation : UserControllerTests
175220
{
176221
[Fact]

RPThreadTrackerV3.BackEnd.Test/Infrastructure/Services/AuthServiceTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,50 @@ public void ThrowsExceptionIfAddingToRoleUnsuccessful()
726726
}
727727
}
728728

729+
public class DeleteAccount : AuthServiceTests
730+
{
731+
[Fact]
732+
public async Task DeletesSuccessfully()
733+
{
734+
// Arrange
735+
var user = new ClaimsPrincipal();
736+
_mockUserManager.Setup(m => m.DeleteAsync(It.IsAny<IdentityUser>())).Returns(Task.FromResult(IdentityResult.Success));
737+
_mockUserManager.Setup(m => m.GetUserAsync(It.Is<ClaimsPrincipal>(p => ReferenceEquals(p, user))))
738+
.Returns(Task.FromResult(new IdentityUser("my-username")));
739+
740+
// Act
741+
await _authService.DeleteAccount(user, _mockUserManager.Object);
742+
743+
// Assert
744+
_mockUserManager.Verify(m => m.DeleteAsync(It.Is<IdentityUser>(u => u.UserName == "my-username")), Times.Once);
745+
}
746+
747+
[Fact]
748+
public void ThrowsExceptionIfDeletionUnsuccessful()
749+
{
750+
// Arrange
751+
var user = new ClaimsPrincipal();
752+
_mockUserManager.Setup(m => m.GetUserAsync(It.Is<ClaimsPrincipal>(p => ReferenceEquals(p, user))))
753+
.Returns(Task.FromResult(new IdentityUser("my-username")));
754+
var failureResult = IdentityResult.Failed(new List<IdentityError>
755+
{
756+
new IdentityError { Description = "Test Error 1" },
757+
new IdentityError { Description = "Test Error 2" }
758+
}.ToArray());
759+
_mockUserManager.Setup(m => m.DeleteAsync(It.IsAny<IdentityUser>()))
760+
.Returns(Task.FromResult(failureResult));
761+
762+
// Act
763+
Func<Task> action = async () => await _authService.DeleteAccount(user, _mockUserManager.Object);
764+
765+
// Assert
766+
action.Should().Throw<InvalidAccountDeletionException>()
767+
.Which.Errors.Should().HaveCount(2)
768+
.And.Contain("Test Error 1")
769+
.And.Contain("Test Error 2");
770+
}
771+
}
772+
729773
public class RevokeRefreshToken : AuthServiceTests
730774
{
731775
[Fact]

RPThreadTrackerV3.BackEnd/Controllers/UserController.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,42 @@ public async Task<IActionResult> ChangeAccountInformation([FromBody] ChangeAccou
157157
_logger.LogError(e, $"Error requesting account information change for {User.Identity.Name}");
158158
return StatusCode(500, "An unexpected error occurred.");
159159
}
160-
}
161-
}
160+
}
161+
162+
/// <summary>
163+
/// Processes a request to delete the current user's account.
164+
/// </summary>
165+
/// <returns>
166+
/// HTTP response containing the results of the request<para />
167+
/// <list type="table"><item><term>200 OK</term><description>Response code for successful deletion of account information</description></item>
168+
/// <item><term>400 Bad Request</term><description>Response code for invalid account information deletion request</description></item>
169+
/// <item><term>500 Internal Server Error</term><description>Response code for unexpected errors</description></item></list>
170+
/// </returns>
171+
[HttpDelete]
172+
[Route("")]
173+
[ProducesResponseType(200)]
174+
[ProducesResponseType(400, Type = typeof(List<string>))]
175+
[ProducesResponseType(500)]
176+
public async Task<IActionResult> DeleteAccount()
177+
{
178+
try
179+
{
180+
_logger.LogInformation($"Received request to delete account information for user {UserId}");
181+
var claimsUser = User;
182+
await _authService.DeleteAccount(claimsUser, _userManager);
183+
_logger.LogInformation($"Processed request to delete user data for user {UserId}");
184+
return Ok();
185+
}
186+
catch (InvalidAccountDeletionException e)
187+
{
188+
_logger.LogWarning(e, $"Error deleting account info for {User.Identity.Name}: {e.Errors}");
189+
return BadRequest(new List<string> { "You do not have permission to delete this account." });
190+
}
191+
catch (Exception e)
192+
{
193+
_logger.LogError(e, $"Error deleting account information for {User.Identity.Name}");
194+
return StatusCode(500, "An unexpected error occurred.");
195+
}
196+
}
197+
}
162198
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// <copyright file="InvalidAccountDeletionException.cs" company="Rosalind Wills">
2+
// Copyright (c) Rosalind Wills. All rights reserved.
3+
// Licensed under the GPL v3 license. See LICENSE file in the project root for full license information.
4+
// </copyright>
5+
6+
namespace RPThreadTrackerV3.BackEnd.Infrastructure.Exceptions.Account
7+
{
8+
using System;
9+
using System.Collections.Generic;
10+
11+
/// <summary>
12+
/// The exception that is thrown when there was an error deleting a user's account information.
13+
/// </summary>
14+
/// <seealso cref="Exception" />
15+
public class InvalidAccountDeletionException : Exception
16+
{
17+
/// <summary>
18+
/// Gets or sets the errors resulting from the account deletion failure.
19+
/// </summary>
20+
/// <value>
21+
/// The errors resulting from the account deletion failure.
22+
/// </value>
23+
public List<string> Errors { get; set; }
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="InvalidAccountDeletionException"/> class.
27+
/// </summary>
28+
/// <param name="errors">The errors resulting from the account deletion failure.</param>
29+
public InvalidAccountDeletionException(List<string> errors)
30+
: base("There was an error deleting the users's account information.")
31+
{
32+
Errors = errors;
33+
}
34+
}
35+
}

RPThreadTrackerV3.BackEnd/Infrastructure/Services/AuthService.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,18 @@ public async Task AddUserToRole(IdentityUser user, string role, UserManager<Iden
253253
}
254254
}
255255

256+
/// <inheritdoc />
257+
/// <exception cref="InvalidAccountDeletionException">Thrown if the account deletion could not be completed.</exception>
258+
public async Task DeleteAccount(ClaimsPrincipal claimsUser, UserManager<IdentityUser> userManager)
259+
{
260+
var identityUser = await userManager.GetUserAsync(claimsUser);
261+
var result = await userManager.DeleteAsync(identityUser);
262+
if (!result.Succeeded)
263+
{
264+
throw new InvalidAccountDeletionException(result.Errors.Select(e => e.Description).ToList());
265+
}
266+
}
267+
256268
/// <inheritdoc />
257269
public void RevokeRefreshToken(string refreshToken, IRepository<RefreshToken> refreshTokenRepository)
258270
{

RPThreadTrackerV3.BackEnd/Interfaces/Services/IAuthService.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ public interface IAuthService
171171
/// </returns>
172172
Task AddUserToRole(IdentityUser user, string role, UserManager<IdentityUser> userManager);
173173

174+
/// <summary>
175+
/// Deletes a user's account.
176+
/// </summary>
177+
/// <param name="claimsUser">The claims principal.</param>
178+
/// <param name="userManager">The user manager.</param>
179+
/// <returns>
180+
/// A task that represents the asynchronous operation.
181+
/// </returns>
182+
Task DeleteAccount(ClaimsPrincipal claimsUser, UserManager<IdentityUser> userManager);
183+
174184
/// <summary>
175185
/// Revokes the given refresh token.
176186
/// </summary>

appveyor.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
ps: |
1818
$env:PATH = 'C:\msys64\usr\bin;' + $env:PATH
1919
Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh
20-
bash codecov.sh -f "RPThreadTrackerV3.BackEnd.Test/coverage.opencover.xml"
20+
bash codecov.sh -f "RPThreadTrackerV3.BackEnd.Test/coverage.opencover.xml" -U "-s" -A "-s"
2121
artifacts:
2222
- path: '\RPThreadTrackerV3.BackEnd\bin\Release\netcoreapp2.1\publish'
2323
name: WebSite
@@ -50,7 +50,7 @@
5050
ps: |
5151
$env:PATH = 'C:\msys64\usr\bin;' + $env:PATH
5252
Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh
53-
bash codecov.sh -f "RPThreadTrackerV3.BackEnd.Test/coverage.opencover.xml"
53+
bash codecov.sh -f "RPThreadTrackerV3.BackEnd.Test/coverage.opencover.xml" -U "-s" -A "-s"
5454
artifacts:
5555
- path: '\RPThreadTrackerV3.BackEnd\bin\Release\netcoreapp2.1\publish'
5656
name: WebSite

0 commit comments

Comments
 (0)