From a27952060b5f71bc67a17973f88bc69d30a0e89e Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 11:26:28 +0200 Subject: [PATCH 1/9] Make all authorized and small cleanup --- exercise.tests/IntegrationTests/PostTests.cs | 1 - exercise.wwwapi/Endpoints/CohortEndpoints.cs | 3 +- exercise.wwwapi/Endpoints/LogEndpoints.cs | 9 ++- exercise.wwwapi/Endpoints/PostEndpoints.cs | 28 +++++--- exercise.wwwapi/Endpoints/SecureApi.cs | 8 +-- exercise.wwwapi/Endpoints/UserEndpoints.cs | 68 +++++++++++--------- exercise.wwwapi/Repository/IRepository.cs | 2 +- 7 files changed, 71 insertions(+), 48 deletions(-) diff --git a/exercise.tests/IntegrationTests/PostTests.cs b/exercise.tests/IntegrationTests/PostTests.cs index 30d6f6c..a3e39d0 100644 --- a/exercise.tests/IntegrationTests/PostTests.cs +++ b/exercise.tests/IntegrationTests/PostTests.cs @@ -633,7 +633,6 @@ public async Task GetPostsByUser_NotFound() [Test] public async Task GetCommentsByUser_Success() { - //Assert get token var token = await LoginAndGetToken("oyvind.perez1@example.com", "SuperHash!4"); diff --git a/exercise.wwwapi/Endpoints/CohortEndpoints.cs b/exercise.wwwapi/Endpoints/CohortEndpoints.cs index 62b0272..3998459 100644 --- a/exercise.wwwapi/Endpoints/CohortEndpoints.cs +++ b/exercise.wwwapi/Endpoints/CohortEndpoints.cs @@ -9,8 +9,9 @@ public static void ConfigureCohortEndpoints(this WebApplication app) var cohorts = app.MapGroup("cohorts"); cohorts.MapPost("/", CreateCohort).WithSummary("Create a cohort"); } + [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task CreateCohort() + private static async Task CreateCohort() { return TypedResults.Ok(); } diff --git a/exercise.wwwapi/Endpoints/LogEndpoints.cs b/exercise.wwwapi/Endpoints/LogEndpoints.cs index 9fce6c0..828e681 100644 --- a/exercise.wwwapi/Endpoints/LogEndpoints.cs +++ b/exercise.wwwapi/Endpoints/LogEndpoints.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; namespace exercise.wwwapi.Endpoints { @@ -10,10 +11,12 @@ public static void ConfigureLogEndpoints(this WebApplication app) logs.MapPost("/", CreateDeliveryLog).WithSummary("Create a delivery log"); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public static async Task CreateDeliveryLog() + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static Task CreateDeliveryLog() { - return TypedResults.Ok(); + return Task.FromResult(TypedResults.Ok()); } } } diff --git a/exercise.wwwapi/Endpoints/PostEndpoints.cs b/exercise.wwwapi/Endpoints/PostEndpoints.cs index 90b2c6b..78bf577 100644 --- a/exercise.wwwapi/Endpoints/PostEndpoints.cs +++ b/exercise.wwwapi/Endpoints/PostEndpoints.cs @@ -37,9 +37,10 @@ public static void ConfigurePostEndpoints(this WebApplication app) [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public static IResult CreatePost( + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + private static IResult CreatePost( IRepository userservice, IRepository postservice, IMapper mapper, @@ -74,9 +75,11 @@ CreatePostDTO request return Results.Created($"/posts/{post.Id}", response); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - public static IResult GetAllPosts(IRepository service, IMapper mapper) + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + private static IResult GetAllPosts(IRepository service, IMapper mapper) { IEnumerable results = service.GetWithIncludes(q => q.Include(p => p.User).Include(p => p.Comments).ThenInclude(c => c.User)); IEnumerable postDTOs = mapper.Map>(results); @@ -91,9 +94,10 @@ public static IResult GetAllPosts(IRepository service, IMapper mapper) [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public static IResult UpdatePost(IRepository service, IMapper mapper, ClaimsPrincipal user, int postid, UpdatePostDTO request) + private static IResult UpdatePost(IRepository service, IMapper mapper, ClaimsPrincipal user, int postid, UpdatePostDTO request) { if (string.IsNullOrWhiteSpace(request.Content)) return TypedResults.BadRequest(new ResponseDTO{ Message = "Content cannot be empty" @@ -128,8 +132,9 @@ public static IResult UpdatePost(IRepository service, IMapper mapper, Clai [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult DeletePost(IRepository service, ClaimsPrincipal user, int postid) { Post? post = service.GetById(postid, q => q.Include(p => p.User).Include(p => p.Comments).ThenInclude(c => c.User)); @@ -151,8 +156,9 @@ private static IResult DeletePost(IRepository service, ClaimsPrincipal use [Authorize] [ProducesResponseType(StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult AddCommentToPost( IRepository commentService, IRepository postService, @@ -201,6 +207,7 @@ private static IResult AddCommentToPost( [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] private static IResult GetCommentsForPost(IRepository postservice, IMapper mapper, int postId) { Post? post = postservice.GetById(postId, q => q.Include(p => p.User).Include(p => p.Comments).ThenInclude(c => c.User)); @@ -209,10 +216,12 @@ private static IResult GetCommentsForPost(IRepository postservice, IMapper List commentsDTO = mapper.Map>(comments); return TypedResults.Ok(new ResponseDTO> { Message = "Success", Data = commentsDTO }); } - + + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult UpdateComment( IRepository service, IMapper mapper, @@ -250,6 +259,7 @@ private static IResult UpdateComment( [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult DeleteCommentById(IRepository service, ClaimsPrincipal user, int commentId) { @@ -276,6 +286,7 @@ private static IResult DeleteCommentById(IRepository service, Claim [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult GetPostsByUser(IRepository service, IMapper mapper, int userid) { @@ -293,6 +304,7 @@ private static IResult GetPostsByUser(IRepository service, IMapper mapper, [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] private static IResult GetCommentsByUser(IRepository service, IMapper mapper, int userid) { diff --git a/exercise.wwwapi/Endpoints/SecureApi.cs b/exercise.wwwapi/Endpoints/SecureApi.cs index 4bf7a74..4d63b1b 100644 --- a/exercise.wwwapi/Endpoints/SecureApi.cs +++ b/exercise.wwwapi/Endpoints/SecureApi.cs @@ -12,16 +12,14 @@ public static class SecureApi public static void ConfigureSecureApi(this WebApplication app) { app.MapGet("message", GetMessage); - - } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task GetMessage(IRepository service, ClaimsPrincipal user) + private static Task GetMessage(IRepository service, ClaimsPrincipal user) { - - return TypedResults.Ok(new { LoggedIn = true, UserId = user.UserRealId().ToString(), Email = $"{user.Email()}", Message = "Pulled the userid and email out of the claims" }); + return Task.FromResult(TypedResults.Ok(new { LoggedIn = true, UserId = user.UserRealId().ToString(), Email = $"{user.Email()}", Message = "Pulled the userid and email out of the claims" })); } } } diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs index dd1ee34..1d7a57b 100644 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -20,23 +20,26 @@ public static class UserEndpoints { public static void ConfigureAuthApi(this WebApplication app) { + app.MapPost("/login", Login).WithSummary("Localhost Login"); + var users = app.MapGroup("users"); users.MapPost("/", Register).WithSummary("Create user"); users.MapGet("/", GetUsers).WithSummary("Get all users by first name if provided"); - users.MapGet("/{id}", GetUserById).WithSummary("Get user by user id"); - users.MapPatch("/{id}", UpdateUser).WithSummary("Update a user"); - app.MapPost("/login", Login).WithSummary("Localhost Login"); + users.MapGet("/{id:int}", GetUserById).WithSummary("Get user by user id"); + users.MapPatch("/{id:int}", UpdateUser).WithSummary("Update a user"); } + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task GetUsers(IRepository service, string? name, ClaimsPrincipal user) + private static async Task GetUsers(IRepository repository, ClaimsPrincipal claims, string? name) { - IEnumerable results = await service.Get(); + IEnumerable results = await repository.Get(); string? search = name?.Trim().ToLower(); + UsersSuccessDTO userData = new UsersSuccessDTO() { - Users = !string.IsNullOrWhiteSpace(name) + Users = !string.IsNullOrWhiteSpace(search) ? results.Where(i => (i.FirstName.ToLower().Contains(search)) || (i.LastName.ToLower().Contains(search)) || @@ -44,20 +47,22 @@ private static async Task GetUsers(IRepository service, string? n ).ToList() : results.ToList() }; + ResponseDTO response = new ResponseDTO() { Message = "success", Data = userData }; + return TypedResults.Ok(response); } - + + // Public endpoint, not authorized [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] - private static IResult Register(RegisterRequestDTO request, IRepository service, IMapper mapper) + private static IResult Register(IRepository repository, IMapper mapper, RegisterRequestDTO request) { - // syntax checks // check valid password string validationResult = Validator.Password(request.password); @@ -67,7 +72,7 @@ private static IResult Register(RegisterRequestDTO request, IRepository se if (emailValidation != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = emailValidation }); // check if email is in database - var emailExists = service.GetAllFiltered(q => q.Email == request.email); + var emailExists = repository.GetAllFiltered(q => q.Email == request.email); if (emailExists.Count() != 0) return Results.Conflict(new ResponseDTO() { Message = "Fail" }); @@ -78,8 +83,8 @@ private static IResult Register(RegisterRequestDTO request, IRepository se user.PasswordHash = passwordHash; user.Email = request.email; - service.Insert(user); - service.Save(); + repository.Insert(user); + repository.Save(); ResponseDTO response = new ResponseDTO { @@ -89,11 +94,11 @@ private static IResult Register(RegisterRequestDTO request, IRepository se return Results.Created($"/users/{user.Id}", response); } - + + // Public endpoint (not authorized) [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static IResult Login(LoginRequestDTO request, IRepository service, IConfigurationSettings config, IMapper mapper) + private static IResult Login(IRepository repository, IMapper mapper, LoginRequestDTO request, IConfigurationSettings config) { //if (string.IsNullOrEmpty(request.username)) request.username = request.email; @@ -102,17 +107,15 @@ private static IResult Login(LoginRequestDTO request, IRepository service, // Check for valid password string validationResult = Validator.Password(request.password); Console.WriteLine($"Password: {validationResult}"); - if (validationResult != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid email and/or password provided" }); + if (validationResult != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid bruh 3 ded" }); //email doesn't exist, should probably be 404 user not found, but should maybe just say invalid email or password //check if email is in database - var emailExists = service.GetAllFiltered(q => q.Email == request.email); - if (emailExists.Count() == 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid email and/or password provided" }); - - - - User user = service.GetAll().FirstOrDefault(u => u.Email == request.email)!; + var emailExists = repository.GetAllFiltered(q => q.Email == request.email); + if (emailExists.Count() == 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid emadagffdsghgfsdfgsgsdfgdfsrovided" }); + + User user = repository.GetAll().FirstOrDefault(u => u.Email == request.email)!; if (!BCrypt.Net.BCrypt.Verify(request.password, user.PasswordHash)) @@ -137,11 +140,14 @@ private static IResult Login(LoginRequestDTO request, IRepository service, return Results.Ok(response); } + + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public static async Task GetUserById(IRepository service, int id, IMapper mapper) + private static async Task GetUserById(IRepository repository, ClaimsPrincipal claims, IMapper mapper, int id) { - var user = service.GetById(id); + var user = repository.GetById(id); if (user == null) return TypedResults.NotFound(new ResponseDTO { Message = "User not found" }); ResponseDTO response = new ResponseDTO @@ -152,11 +158,13 @@ public static async Task GetUserById(IRepository service, int id, return TypedResults.Ok(response); } - + + [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public static async Task UpdateUser(IRepository repository, int id, UserPatchDTO userPatch, IMapper mapper) + private static async Task UpdateUser(IRepository repository, ClaimsPrincipal claims, IMapper mapper, int id, UserPatchDTO userPatch) { if (userPatch.GetType().GetProperties().Length > 0 && userPatch.GetType().GetProperties().All((p) => p.GetValue(userPatch) == null)) return TypedResults.BadRequest(new ResponseDTO() { Message = "Provide at least one field for update" }); @@ -229,16 +237,18 @@ public static async Task UpdateUser(IRepository repository, int i return TypedResults.Ok(response); } + + // Helper, creates jwt tokens private static string CreateToken(User user, IConfigurationSettings config) { - List claims = new List - { + List claims = + [ new Claim(ClaimTypes.Sid, user.Id.ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, user.Username), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Role, ((int)user.Role).ToString()) - }; + ]; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.GetValue("AppSettings:Token"))); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); diff --git a/exercise.wwwapi/Repository/IRepository.cs b/exercise.wwwapi/Repository/IRepository.cs index 4521d08..ddeaa7d 100644 --- a/exercise.wwwapi/Repository/IRepository.cs +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -12,7 +12,7 @@ public interface IRepository where T : class IEnumerable GetAllFiltered(Expression> filter); IEnumerable GetWithIncludes(Func, IQueryable> includeQuery); T? GetById(int id, Func, IQueryable> includeQuery); - T GetById(object id); + T? GetById(object id); void Insert(T obj); void Update(T obj); void Delete(object id); From d7aabab2a0ea07e08a85c971f83b6d76f1e167f3 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 11:35:03 +0200 Subject: [PATCH 2/9] Cleaned up validation enpoint --- .../Endpoints/ValidationEndpoint.cs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/exercise.wwwapi/Endpoints/ValidationEndpoint.cs b/exercise.wwwapi/Endpoints/ValidationEndpoint.cs index 4902855..32861c2 100644 --- a/exercise.wwwapi/Endpoints/ValidationEndpoint.cs +++ b/exercise.wwwapi/Endpoints/ValidationEndpoint.cs @@ -23,7 +23,7 @@ public static void ConfigureValidationEndpoint(this WebApplication app) /// Source: https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/iam-configuration-reference/username-considerations-for-external-authentication /// /// A object used to query the user data source for existing GitHub usernames. - /// The username string to validate. + /// The username string to validate. /// /// 200 OK response with a message if the username is accepted.
/// 400 Bad Request with a message if the username is invalid or if username already exists in database. @@ -34,11 +34,12 @@ public static void ConfigureValidationEndpoint(this WebApplication app) [ProducesResponseType(StatusCodes.Status400BadRequest)] private static IResult ValidateGitUsername(IRepository repository, [FromQuery] string username) { - if (username == null || string.IsNullOrEmpty(username)) return TypedResults.BadRequest(new ResponseDTO() { Message = "Something went wrong!" }); string result = Helpers.Validator.Username(username); if (result != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = result }); + var gitUsernameExists = repository.GetAllFiltered(q => q.GithubUsername == username); if (gitUsernameExists.Count() != 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "GitHub username is already in use" }); + return TypedResults.Ok(new ResponseDTO() { Message = result }); } @@ -57,18 +58,19 @@ private static IResult ValidateGitUsername(IRepository repository, [FromQu [ProducesResponseType(StatusCodes.Status400BadRequest)] private static IResult ValidateEmail(IRepository repository, string email) { - if (email == null || string.IsNullOrEmpty(email)) return TypedResults.BadRequest(new ResponseDTO() { Message = "Something went wrong!" }); string result = Helpers.Validator.Email(email); if (result != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = result }); + var emailExists = repository.GetAllFiltered(q => q.Email == email); if (emailExists.Count() != 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Email already exists" }); + return TypedResults.Ok(new ResponseDTO() { Message = result }); } /// /// Validates a password using custom password rules. /// - /// A object containing the password to validate. + /// A object containing the password to validate. /// /// 200 OK response if the password is accepted.
/// 400 Bad Request with a message if the password is invalid or if validation fails. @@ -77,14 +79,14 @@ private static IResult ValidateEmail(IRepository repository, string email) /// Password is invalid or validation failed. [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - private static IResult ValidatePassword(PasswordDTO passwordDTO) + private static IResult ValidatePassword(PasswordDTO? passwordDto) { - if (passwordDTO == null || string.IsNullOrEmpty(passwordDTO.password)) - return TypedResults.BadRequest(new ResponseDTO() { Message = "Something went wrong!" }); - string result = Helpers.Validator.Password(passwordDTO.password); - if (result == null) return TypedResults.BadRequest(new ResponseDTO() { Message = "Something went wrong!" }); - else if (result == "Accepted") return TypedResults.Ok(new ResponseDTO() { Message = result }); - else return TypedResults.BadRequest(new ResponseDTO() { Message = result }); + if (passwordDto == null || string.IsNullOrEmpty(passwordDto.password)) return TypedResults.BadRequest(new ResponseDTO() { Message = "Something went wrong!" }); + + string result = Helpers.Validator.Password(passwordDto.password); + if (result == "Accepted") return TypedResults.Ok(new ResponseDTO() { Message = result }); + + return TypedResults.BadRequest(new ResponseDTO() { Message = result }); } /// @@ -104,11 +106,12 @@ private static IResult ValidatePassword(PasswordDTO passwordDTO) [ProducesResponseType(StatusCodes.Status400BadRequest)] private static IResult ValidateUsername(IRepository repository, [FromQuery] string username) { - if (username == null || string.IsNullOrEmpty(username)) return TypedResults.BadRequest(new ResponseDTO() { Message = "Empty input" }); string result = Helpers.Validator.Username(username); if (result != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = result }); + var usernameExists = repository.GetAllFiltered(q => q.Username == username); if (usernameExists.Count() != 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Username is already in use" }); + return TypedResults.Ok(new ResponseDTO() { Message = result }); } } From 02b09d1fa76cff3b02035a8357958d0d6f46e8d7 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 12:46:07 +0200 Subject: [PATCH 3/9] Tests are repaired --- exercise.tests/IntegrationTests/UserTests.cs | 106 +++++++------------ exercise.wwwapi/Endpoints/UserEndpoints.cs | 46 +++++--- 2 files changed, 71 insertions(+), 81 deletions(-) diff --git a/exercise.tests/IntegrationTests/UserTests.cs b/exercise.tests/IntegrationTests/UserTests.cs index 4837cc6..5edf487 100644 --- a/exercise.tests/IntegrationTests/UserTests.cs +++ b/exercise.tests/IntegrationTests/UserTests.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using exercise.tests.Helpers; +using exercise.wwwapi.DTOs; namespace exercise.tests.IntegrationTests { @@ -13,26 +14,8 @@ namespace exercise.tests.IntegrationTests /// Integration tests exercising the user management endpoints end-to-end via the API surface. /// [TestFixture] - public class UserTests + public class UserTests : BaseIntegrationTest { - private WebApplicationFactory _factory; - private HttpClient _client; - - [SetUp] - public void SetUp() - { - // Arrange - _factory = new WebApplicationFactory(); - _client = _factory.CreateClient(); - } - - [TearDown] - public void TearDown() - { - _client.Dispose(); - _factory.Dispose(); - } - /// /// Confirms that valid registration payloads yield an HTTP 201 Created response. /// @@ -193,19 +176,18 @@ public async Task Login_failure(string email, string password) [Test] public async Task UpdateUserSuccess() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "username", "roman-olsen13"}, - { "email", "roman.olsen13@example.com"}, - { "password", "aGoodPassword!200" }, - { "role", 0} + Username = "roman-olsen13", + Email = "roman.olsen13@example.com", + Password = "aGoodPassword!200" , + Role = 0 }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } @@ -216,14 +198,12 @@ public async Task UpdateUserSuccess() [Test] public async Task UpdateUserNullFieldsOnly() { - var fieldsToUpdate = new Dictionary { }; - - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - + var fieldsToUpdate = new UserPatchDTO(); + + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 1; - var response = await _client.PatchAsync($"/users/{userId}", content); - + + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -233,17 +213,15 @@ public async Task UpdateUserNullFieldsOnly() [Test] public async Task UpdateUserInvalidUsername() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "username", "roman--olsen13"} + GithubUsername = "roman--olsen13" }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); - + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); + + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -253,17 +231,15 @@ public async Task UpdateUserInvalidUsername() [Test] public async Task UpdateUserInvalidGitHubUsername() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "gitHubUsername", "roman--olsen13"} + GithubUsername = "roman--olsen13" }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); - + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); + + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -273,9 +249,9 @@ public async Task UpdateUserInvalidGitHubUsername() [Test] public async Task UpdateUserInvalidEmail() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "email", "roman.olsen13@.e.com"} + Email = "roman.olsen13@.e.com" }; var json = JsonSerializer.Serialize(fieldsToUpdate); @@ -353,16 +329,15 @@ public async Task UpdateUserUsernameExists() [Test] public async Task UpdateUserGitHubUsernameExists() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "gitHubUsername", "nigel-nowak2"} + GithubUsername = "nigel-nowak2" }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); + + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -373,16 +348,15 @@ public async Task UpdateUserGitHubUsernameExists() [Test] public async Task UpdateUserEmailExists() { - var fieldsToUpdate = new Dictionary + var fieldsToUpdate = new UserPatchDTO() { - { "email", "nigel.nowak2@example.com"} + Email="nigel.nowak2@example.com" }; - - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - + int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); + + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -396,7 +370,9 @@ public async Task UpdateUserEmailExists() [TestCase("10000000", HttpStatusCode.NotFound)] public async Task GetUserByIdTest(string id, HttpStatusCode responseStatus) { - var response = await _client.GetAsync($"/users/{id}"); + string token = await LoginAndGetToken(TeacherEmail, TeacherPassword); + + var response = await SendAuthenticatedGetAsync($"/users/{id}", token); Assert.That(response.StatusCode, Is.EqualTo(responseStatus)); } diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs index 1d7a57b..386d275 100644 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -29,6 +29,22 @@ public static void ConfigureAuthApi(this WebApplication app) users.MapPatch("/{id:int}", UpdateUser).WithSummary("Update a user"); } + /// + /// Retrieves users, optionally filtered by a case-insensitive search on first name, last name, or full name. + /// + /// + /// The user repository used to fetch users. + /// + /// + /// -user that authorizes the user to use this endpoint. + /// + /// + /// Optional search term to filter users by first name, last name, or "FirstName LastName". + /// + /// + /// with a 200 OK containing a of on success, + /// or a 401 Unauthorized if the caller is not authorized. + /// [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -57,32 +73,29 @@ private static async Task GetUsers(IRepository repository, Claims return TypedResults.Ok(response); } + // TODO: Docstring // Public endpoint, not authorized [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] private static IResult Register(IRepository repository, IMapper mapper, RegisterRequestDTO request) { - // syntax checks - // check valid password + // Validate password string validationResult = Validator.Password(request.password); if (validationResult != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = validationResult }); - // check valid email + + // Validate email string emailValidation = Validator.Email(request.email); if (emailValidation != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = emailValidation }); - - // check if email is in database var emailExists = repository.GetAllFiltered(q => q.Email == request.email); if (emailExists.Count() != 0) return Results.Conflict(new ResponseDTO() { Message = "Fail" }); - - - string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.password); - - var user = new User(); - - user.PasswordHash = passwordHash; - user.Email = request.email; - + + // Put user + var user = new User + { + PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.password), + Email = request.email + }; repository.Insert(user); repository.Save(); @@ -95,6 +108,7 @@ private static IResult Register(IRepository repository, IMapper mapper, Re return Results.Created($"/users/{user.Id}", response); } + // TODO: Docstring // Public endpoint (not authorized) [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -107,13 +121,13 @@ private static IResult Login(IRepository repository, IMapper mapper, Login // Check for valid password string validationResult = Validator.Password(request.password); Console.WriteLine($"Password: {validationResult}"); - if (validationResult != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid bruh 3 ded" }); + if (validationResult != "Accepted") return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid email and/or password provided" }); //email doesn't exist, should probably be 404 user not found, but should maybe just say invalid email or password //check if email is in database var emailExists = repository.GetAllFiltered(q => q.Email == request.email); - if (emailExists.Count() == 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid emadagffdsghgfsdfgsgsdfgdfsrovided" }); + if (emailExists.Count() == 0) return TypedResults.BadRequest(new ResponseDTO() { Message = "Invalid email and/or password provided" }); User user = repository.GetAll().FirstOrDefault(u => u.Email == request.email)!; From ca12a9537f83b1b27a6666d843ac53c9cd832866 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:04:00 +0200 Subject: [PATCH 4/9] Forgot to repair some tests --- exercise.tests/IntegrationTests/UserTests.cs | 22 ++++++++------------ 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/exercise.tests/IntegrationTests/UserTests.cs b/exercise.tests/IntegrationTests/UserTests.cs index 5edf487..4021d19 100644 --- a/exercise.tests/IntegrationTests/UserTests.cs +++ b/exercise.tests/IntegrationTests/UserTests.cs @@ -254,11 +254,10 @@ public async Task UpdateUserInvalidEmail() Email = "roman.olsen13@.e.com" }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); + var token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -274,12 +273,11 @@ public async Task UpdateUserInvalidPassword() { "password", "nope!"} }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); + var token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); - + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -294,11 +292,10 @@ public async Task UpdateUserInvalidRole() { "role", 10} }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); + var token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } @@ -314,11 +311,10 @@ public async Task UpdateUserUsernameExists() { "username", "nigel-nowak2"} }; - var json = JsonSerializer.Serialize(fieldsToUpdate); - var content = new StringContent(json, Encoding.UTF8, "application/json"); + var token = await LoginAndGetToken(TeacherEmail, TeacherPassword); int userId = 13; - var response = await _client.PatchAsync($"/users/{userId}", content); + var response = await SendAuthenticatedPatchAsync($"/users/{userId}", token, fieldsToUpdate); Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } From 57a5110f7c27aedc0fd249ca4f0e69ece83c4965 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:08:06 +0200 Subject: [PATCH 5/9] Update PostTests.cs --- exercise.tests/IntegrationTests/PostTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise.tests/IntegrationTests/PostTests.cs b/exercise.tests/IntegrationTests/PostTests.cs index 8d1e503..b7c5f8c 100644 --- a/exercise.tests/IntegrationTests/PostTests.cs +++ b/exercise.tests/IntegrationTests/PostTests.cs @@ -44,7 +44,7 @@ public async Task GetAllPosts() using (Assert.EnterMultipleScope()) { Assert.That(post?["id"]?.GetValue(), Is.GreaterThan(0)); - Assert.That(post["content"]?.GetValue(), Is.Not.Null); + Assert.That(post?["content"]?.GetValue(), Is.Not.Null); Assert.That(post["numLikes"]?.GetValue(), Is.Not.Null); Assert.That(post["createdAt"], Is.Not.Null); } From 63437e2c1db95a2cf5ada28050159eda3a614b55 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:11:31 +0200 Subject: [PATCH 6/9] Update UserTests.cs --- exercise.tests/IntegrationTests/UserTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise.tests/IntegrationTests/UserTests.cs b/exercise.tests/IntegrationTests/UserTests.cs index 4021d19..50f92d5 100644 --- a/exercise.tests/IntegrationTests/UserTests.cs +++ b/exercise.tests/IntegrationTests/UserTests.cs @@ -215,7 +215,7 @@ public async Task UpdateUserInvalidUsername() { var fieldsToUpdate = new UserPatchDTO() { - GithubUsername = "roman--olsen13" + Username = "roman--olsen13" }; int userId = 13; From 201c299a1c3ab6cac47169fb4afa549bf83bd277 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:16:58 +0200 Subject: [PATCH 7/9] Update UserEndpoints.cs --- exercise.wwwapi/Endpoints/UserEndpoints.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs index 386d275..6117f52 100644 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -35,9 +35,6 @@ public static void ConfigureAuthApi(this WebApplication app) /// /// The user repository used to fetch users. /// - /// - /// -user that authorizes the user to use this endpoint. - /// /// /// Optional search term to filter users by first name, last name, or "FirstName LastName". /// @@ -48,7 +45,7 @@ public static void ConfigureAuthApi(this WebApplication app) [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task GetUsers(IRepository repository, ClaimsPrincipal claims, string? name) + private static async Task GetUsers(IRepository repository, string? name) { IEnumerable results = await repository.Get(); string? search = name?.Trim().ToLower(); From ee233e24fb1c05b2100a80159de68e8a6e2dfd09 Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:26:35 +0200 Subject: [PATCH 8/9] Update UserEndpoints.cs --- exercise.wwwapi/Endpoints/UserEndpoints.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs index 6117f52..67db7c2 100644 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -35,6 +35,9 @@ public static void ConfigureAuthApi(this WebApplication app) /// /// The user repository used to fetch users. /// + /// + /// -user that authorizes the user to use this endpoint. + /// /// /// Optional search term to filter users by first name, last name, or "FirstName LastName". /// @@ -45,8 +48,15 @@ public static void ConfigureAuthApi(this WebApplication app) [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - private static async Task GetUsers(IRepository repository, string? name) + private static async Task GetUsers(IRepository repository, ClaimsPrincipal claims, string? name) { + int? id = claims.UserRealId(); + if (id == null) + { + return TypedResults.Ok(new ResponseDTO() + { Message = "Invalid token" }); + } + IEnumerable results = await repository.Get(); string? search = name?.Trim().ToLower(); From 1784b0178bd8d5385daf702d9e54de122af3e7ef Mon Sep 17 00:00:00 2001 From: reduan-azouaghe Date: Mon, 22 Sep 2025 13:30:41 +0200 Subject: [PATCH 9/9] check role on update --- exercise.wwwapi/Endpoints/UserEndpoints.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/exercise.wwwapi/Endpoints/UserEndpoints.cs b/exercise.wwwapi/Endpoints/UserEndpoints.cs index 67db7c2..01b5a71 100644 --- a/exercise.wwwapi/Endpoints/UserEndpoints.cs +++ b/exercise.wwwapi/Endpoints/UserEndpoints.cs @@ -193,6 +193,15 @@ private static async Task UpdateUser(IRepository repository, Clai var user = repository.GetById(id); if (user == null) return TypedResults.NotFound(new ResponseDTO { Message = "User not found" }); + + if (user.Id != claims.UserRealId() && claims.Role() == (int)Roles.student) + { + var forbiddenResponse = new ResponseDTO + { + Message = "You are not authorized to edit this post." + }; + return TypedResults.Json(forbiddenResponse, statusCode: StatusCodes.Status403Forbidden); + } if (userPatch.Username != null && userPatch.Username != user.Username) {