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.
This commit is contained in:
Artemy
2025-06-30 12:47:53 +07:00
parent 9bfcc1980d
commit 7aa1b1831a
7 changed files with 140 additions and 86 deletions
@@ -201,6 +201,40 @@ public class UsersRepositoryTests
await userRepository.FindByRangeUsernamesAsync(usernames));
}
[Test]
public async Task Given_ValidQuery_When_SearchPotentialFriendsAsync_Then_Returns_Users()
{
// Arrange
var random = new Random();
var users = _fixture.CreateMany<User>(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<NotFoundByKeyException<(string,Guid)>>(async () => await
userRepository.SearchPotentialFriendsAsync(_fixture.Create<Guid>(), _fixture.Create<string>()));
}
[Test]
public async Task Given_ValidDateOnly_When_FindByCreatedDate_Then_Returns_Users()
{
+66 -60
View File
@@ -21,97 +21,122 @@ public class FriendsController : Controller
_friendsService = friendsService;
}
[HttpGet("search")] //api/friends/search
[HttpGet("search")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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." });
}
}
@@ -125,39 +150,20 @@ public class FriendsController : Controller
return userId;
}
private List<UserDto> BuildUserDtos(IEnumerable<User> users)
private List<UserDto> BuildUserDtos(IEnumerable<User> users) => users.Select(user => new UserDto
{
List<UserDto> userDtos = new List<UserDto>();
Id = user.Id,
Username = user.Username,
Description = user.Description,
WasOnline = user.WasOnline,
IconId = user.IconId
}).ToList();
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<FriendshipDto> BuildFriendshipDtos(List<Friendship> result)
private List<FriendshipDto> BuildFriendshipDtos(IEnumerable<Friendship> friendships) => friendships.Select(f => new FriendshipDto
{
List<FriendshipDto> dtos = new List<FriendshipDto>();
foreach (var friendship in result)
{
dtos.Add(new FriendshipDto()
{
Id = friendship.Id,
Status = friendship.Status,
AddresseeId = friendship.AddresseeId,
RequesterId = friendship.RequesterId,
});
}
return dtos;
}
Id = f.Id,
Status = f.Status,
AddresseeId = f.AddresseeId,
RequesterId = f.RequesterId
}).ToList();
}
-5
View File
@@ -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();
@@ -4,7 +4,7 @@ namespace Govor.Application.Interfaces;
public interface IFriendsService
{
Task<List<User>> SearchUsersAsync(string searchTerm, Guid currentId);
Task<List<User>> SearchUsersAsync(string query, Guid currentId);
Task SendFriendRequestAsync(Guid fromUserId, Guid toUserId);
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
Task<List<User>> GetFriendsAsync(Guid userId);
+15 -13
View File
@@ -19,12 +19,14 @@ public class FriendsService : IFriendsService
_friendshipsRepository = relationshipsRepository;
}
public async Task<List<User>> SearchUsersAsync(string searchTerm, Guid currentId)
public async Task<List<User>> SearchUsersAsync(string query, Guid currentId)
{
var all = await _usersRepository.SearchPotentialFriendsAsync(currentId, searchTerm);
List<User> all = new List<User>();
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<Guid> 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<Guid> ex)
{
throw new InvalidOperationException("User not exist", ex);
throw new InvalidOperationException("User not found", ex);
}
}
+17
View File
@@ -15,4 +15,21 @@ public class User
public Invitation? Invite { get; set; }
public List<Friendship> SentFriendRequests { get; set; } = new();
public List<Friendship> 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;
}
}
+1 -1
View File
@@ -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"));
}