From 7aa1b1831a1ba3048bdfdb75eec79d518130bcce Mon Sep 17 00:00:00 2001 From: Artemy <109195690+stalcker2288969@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:47:53 +0700 Subject: [PATCH] Improve friends search and error handling in API Refactored FriendsController to provide more precise error responses and input validation. Enhanced FriendsService to handle new exception types and improved user search logic. Added tests for user search, updated repository to throw on empty search results, and implemented User equality override. Cleaned up Program.cs controller JSON options. --- .../EF/Repositories/UsersRepositoryTests.cs | 36 ++++- Govor.API/Controllers/FriendsController.cs | 134 +++++++++--------- Govor.API/Program.cs | 5 - .../Interfaces/IFriendsService.cs | 2 +- Govor.Application/Services/FriendsService.cs | 30 ++-- Govor.Core/Models/User.cs | 17 +++ Govor.Data/Repositories/UsersRepository.cs | 2 +- 7 files changed, 140 insertions(+), 86 deletions(-) diff --git a/Govor.API.Tests/IntegrationTests/EF/Repositories/UsersRepositoryTests.cs b/Govor.API.Tests/IntegrationTests/EF/Repositories/UsersRepositoryTests.cs index d503c74..7cec3bf 100644 --- a/Govor.API.Tests/IntegrationTests/EF/Repositories/UsersRepositoryTests.cs +++ b/Govor.API.Tests/IntegrationTests/EF/Repositories/UsersRepositoryTests.cs @@ -200,7 +200,41 @@ public class UsersRepositoryTests Assert.ThrowsAsync>>(async () => await userRepository.FindByRangeUsernamesAsync(usernames)); } - + + [Test] + public async Task Given_ValidQuery_When_SearchPotentialFriendsAsync_Then_Returns_Users() + { + // Arrange + var random = new Random(); + var users = _fixture.CreateMany(random.Next(3, 10)).ToList(); + + await using var context = new GovorDbContext(_options); + var userRepository = new UsersRepository(context, _userValidator); + + context.Users.AddRange(users); + await context.SaveChangesAsync(); + + // Act + var result = await userRepository.SearchPotentialFriendsAsync(users[1].Id, users[0].Username[..15]); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result.First().Id, Is.EqualTo(users[0].Id)); + } + + [Test] + public async Task Given_InvalidQuery_When_SearchPotentialFriendsAsync_Should_Throw_NotFoundByKeyException() + { + // Arrange + await using var context = new GovorDbContext(_options); + var userRepository = new UsersRepository(context, _userValidator); + + // Act & Assert + Assert.ThrowsAsync>(async () => await + userRepository.SearchPotentialFriendsAsync(_fixture.Create(), _fixture.Create())); + } + [Test] public async Task Given_ValidDateOnly_When_FindByCreatedDate_Then_Returns_Users() { diff --git a/Govor.API/Controllers/FriendsController.cs b/Govor.API/Controllers/FriendsController.cs index 96492fd..5f1e7c4 100644 --- a/Govor.API/Controllers/FriendsController.cs +++ b/Govor.API/Controllers/FriendsController.cs @@ -20,98 +20,123 @@ public class FriendsController : Controller _logger = logger; _friendsService = friendsService; } - - [HttpGet("search")] //api/friends/search + + [HttpGet("search")] public async Task Search(string query) { + if (string.IsNullOrWhiteSpace(query)) + return BadRequest("Query cannot be empty"); + try { - var result = await _friendsService.SearchUsersAsync(query, GetCurrentUserId()); - - return Ok(BuildUserDtos(result)); + var result = await _friendsService.SearchUsersAsync(query, GetCurrentUserId()); + return Ok(BuildUserDtos(result)); + } + catch (SearchUsersException ex) + { + _logger.LogWarning(ex, ex.Message); + return NotFound(new { error = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, ex.Message); - return BadRequest("Something happened during the search... try again"); + return StatusCode(500, new { error = "Internal error during user search." }); } } - [HttpPost("request")] //api/friends/request + [HttpPost("request")] public async Task SendRequest(Guid targetUserId) { + if (targetUserId == Guid.Empty) + return BadRequest("Target user ID is invalid"); + try { - await _friendsService.SendFriendRequestAsync(targetUserId, GetCurrentUserId()); - return Ok(); + await _friendsService.SendFriendRequestAsync(targetUserId, GetCurrentUserId()); + return Ok(new { message = "Friend request sent successfully." }); } catch (InvalidOperationException ex) { - _logger.LogWarning("The user tried to send a friend request to themselves"); - return BadRequest("You can't send a friend request to themselves"); + _logger.LogWarning(ex, "Tried to send request to self"); + return UnprocessableEntity(new { error = ex.Message }); } catch (RequestAlreadySentException ex) { - _logger.LogWarning("The user tried to send a friend request but the request is already sent"); - return BadRequest("You already sent a friend request"); + _logger.LogWarning(ex, ex.Message); + return Conflict(new { error = ex.Message }); } catch (Exception ex) { _logger.LogError(ex, ex.Message); - return BadRequest("Something happened during the request... try again"); + return StatusCode(500, new { error = "Failed to send friend request." }); } } - [HttpGet("requests")] //api/friends/requests + [HttpGet("requests")] public async Task GetIncomingRequests() { try { - _logger.LogInformation($"Getting incoming requests for user {GetCurrentUserId()}..."); var result = await _friendsService.GetIncomingRequestsAsync(GetCurrentUserId()); return Ok(BuildFriendshipDtos(result)); } catch (InvalidOperationException ex) { - _logger.LogWarning(ex, ex.Message); - return BadRequest("You can't get real friend requests because we can't find your user data! Try again"); + _logger.LogError(ex, ex.Message); + return BadRequest(new { error = "Failed to get friend requests. User data missing." }); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { error = "Internal server error." }); } } - [HttpPost("accept")] //api/friends/accept + [HttpPost("accept")] public async Task AcceptFriend(Guid requesterId) { + if (requesterId == Guid.Empty) + return BadRequest("Requester ID is invalid"); + try { await _friendsService.AcceptFriendRequestAsync(requesterId, GetCurrentUserId()); - return Ok(); + return Ok(new { message = "Friend request accepted." }); } catch (InvalidOperationException ex) { - _logger.LogWarning("The user tried to accept a friend request but the request not exists"); - return BadRequest(ex.Message); + _logger.LogWarning(ex, ex.Message); + return NotFound(new { error = ex.Message }); } catch (UnauthorizedAccessException ex) { - _logger.LogWarning("User tried to accept a friend request but addresseeId not equal userId"); - return BadRequest("You can't accept a friend request"); + _logger.LogWarning(ex, ex.Message); + return Forbid(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { error = "Failed to accept friend request." }); } } - [HttpGet] //api/friends/ + [HttpGet] public async Task GetFriends() { try { - _logger.LogInformation($"Getting friends by user {GetCurrentUserId()}..."); var result = await _friendsService.GetFriendsAsync(GetCurrentUserId()); - return Ok(BuildUserDtos(result)); } - catch(UnauthorizedAccessException ex) + catch (InvalidOperationException ex) { _logger.LogError(ex, ex.Message); - return BadRequest("Something happened during the request... try again"); + return BadRequest(new { error = "User data not found." }); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + return StatusCode(500, new { error = "Internal server error." }); } } @@ -124,40 +149,21 @@ public class FriendsController : Controller } return userId; } - - private List BuildUserDtos(IEnumerable users) + + private List BuildUserDtos(IEnumerable users) => users.Select(user => new UserDto { - List userDtos = new List(); - - foreach (var user in users) - { - userDtos.Add(new UserDto() - { - Id = user.Id, - Description = user.Description, - Username = user.Username, - WasOnline = user.WasOnline, - IconId = user.IconId, - }); - } - return userDtos; - } - - private List BuildFriendshipDtos(List result) + Id = user.Id, + Username = user.Username, + Description = user.Description, + WasOnline = user.WasOnline, + IconId = user.IconId + }).ToList(); + + private List BuildFriendshipDtos(IEnumerable friendships) => friendships.Select(f => new FriendshipDto { - List dtos = new List(); - - foreach (var friendship in result) - { - dtos.Add(new FriendshipDto() - { - Id = friendship.Id, - Status = friendship.Status, - AddresseeId = friendship.AddresseeId, - RequesterId = friendship.RequesterId, - }); - } - - return dtos; - } -} \ No newline at end of file + Id = f.Id, + Status = f.Status, + AddresseeId = f.AddresseeId, + RequesterId = f.RequesterId + }).ToList(); +} diff --git a/Govor.API/Program.cs b/Govor.API/Program.cs index 1b9bad4..bf1019a 100644 --- a/Govor.API/Program.cs +++ b/Govor.API/Program.cs @@ -59,11 +59,6 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthorization(); builder.Services.AddControllers(); -builder.Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve; - }); // Init DI builder.Services.AddServices(); diff --git a/Govor.Application/Interfaces/IFriendsService.cs b/Govor.Application/Interfaces/IFriendsService.cs index 0bcc233..d234395 100644 --- a/Govor.Application/Interfaces/IFriendsService.cs +++ b/Govor.Application/Interfaces/IFriendsService.cs @@ -4,7 +4,7 @@ namespace Govor.Application.Interfaces; public interface IFriendsService { - Task> SearchUsersAsync(string searchTerm, Guid currentId); + Task> SearchUsersAsync(string query, Guid currentId); Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId); Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId); Task> GetFriendsAsync(Guid userId); diff --git a/Govor.Application/Services/FriendsService.cs b/Govor.Application/Services/FriendsService.cs index 01d9068..f2d62fe 100644 --- a/Govor.Application/Services/FriendsService.cs +++ b/Govor.Application/Services/FriendsService.cs @@ -19,12 +19,14 @@ public class FriendsService : IFriendsService _friendshipsRepository = relationshipsRepository; } - public async Task> SearchUsersAsync(string searchTerm, Guid currentId) + public async Task> SearchUsersAsync(string query, Guid currentId) { - var all = await _usersRepository.SearchPotentialFriendsAsync(currentId, searchTerm); - + List all = new List(); + try { + all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query); + var friends = await _friendshipsRepository.FindByUserIdAsync(currentId); friends = friends.Where(f => f.Status == FriendshipStatus.Accepted).ToList(); @@ -33,13 +35,18 @@ public class FriendsService : IFriendsService .Where(u => u.Id != currentId && !friends.Select(f => f.RequesterId).Contains(u.Id)) .ToList(); } + catch (NotFoundByKeyException<(string, Guid)> ex) + { + throw new SearchUsersException( + $"Users with given query: \"{query}\" for user with id {currentId} was not found", ex); + } catch (NotFoundByKeyException ex) { return all.Where(u => u.Id != currentId).ToList(); } catch (Exception ex) { - throw new SearchUsersException($"When we try find friends by pattern {searchTerm} something wrong", ex); + throw new UnauthorizedAccessException($"When we try find friends by pattern {query} something wrong", ex); } } @@ -85,21 +92,16 @@ public class FriendsService : IFriendsService { try { - var user = await _usersRepository.FindByIdAsync(userId); + var friendships = await _friendshipsRepository.FindByUserIdAsync(userId); - var sent = user.SentFriendRequests + return friendships .Where(f => f.Status == FriendshipStatus.Accepted) - .Select(f => f.Addressee); - - var received = user.ReceivedFriendRequests - .Where(f => f.Status == FriendshipStatus.Accepted) - .Select(f => f.Requester); - - return sent.Concat(received).ToList(); + .Select(f => f.RequesterId == userId ? f.Addressee : f.Requester) + .ToList(); } catch (NotFoundByKeyException ex) { - throw new InvalidOperationException("User not exist", ex); + throw new InvalidOperationException("User not found", ex); } } diff --git a/Govor.Core/Models/User.cs b/Govor.Core/Models/User.cs index 7d407a8..616d9d8 100644 --- a/Govor.Core/Models/User.cs +++ b/Govor.Core/Models/User.cs @@ -15,4 +15,21 @@ public class User public Invitation? Invite { get; set; } public List SentFriendRequests { get; set; } = new(); public List ReceivedFriendRequests { get; set; } = new(); + + public override bool Equals(object? obj) + { + var user = obj as User; + + return Id == user.Id && + Username == user.Username && + Description == user.Description && + PasswordHash == user.PasswordHash && + IconId == user.IconId && + CreatedOn == user.CreatedOn && + WasOnline == user.WasOnline && + InviteId == user.InviteId && + Invite == user.Invite && + SentFriendRequests == user.SentFriendRequests && + ReceivedFriendRequests == user.ReceivedFriendRequests; + } } \ No newline at end of file diff --git a/Govor.Data/Repositories/UsersRepository.cs b/Govor.Data/Repositories/UsersRepository.cs index 5ef3c9e..d665b8d 100644 --- a/Govor.Data/Repositories/UsersRepository.cs +++ b/Govor.Data/Repositories/UsersRepository.cs @@ -84,7 +84,7 @@ public class UsersRepository : IUsersRepository (f.RequesterId == u.Id && f.AddresseeId == currentUserId))) .Take(20) .OrderBy(u => u.Username) - .ToListAsync(); + .ToListOrThrowIfEmpty(new NotFoundByKeyException<(string, Guid)>((query, currentUserId), $"Users with given query for user {currentUserId} not found")); }