mirror of
https://github.com/Govor-team/Govor.git
synced 2026-07-21 19:54:55 +00:00
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:
@@ -201,6 +201,40 @@ public class UsersRepositoryTests
|
|||||||
await userRepository.FindByRangeUsernamesAsync(usernames));
|
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]
|
[Test]
|
||||||
public async Task Given_ValidDateOnly_When_FindByCreatedDate_Then_Returns_Users()
|
public async Task Given_ValidDateOnly_When_FindByCreatedDate_Then_Returns_Users()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,97 +21,122 @@ public class FriendsController : Controller
|
|||||||
_friendsService = friendsService;
|
_friendsService = friendsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("search")] //api/friends/search
|
[HttpGet("search")]
|
||||||
public async Task<IActionResult> Search(string query)
|
public async Task<IActionResult> Search(string query)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
return BadRequest("Query cannot be empty");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _friendsService.SearchUsersAsync(query, GetCurrentUserId());
|
var result = await _friendsService.SearchUsersAsync(query, GetCurrentUserId());
|
||||||
|
return Ok(BuildUserDtos(result));
|
||||||
return Ok(BuildUserDtos(result));
|
}
|
||||||
|
catch (SearchUsersException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, ex.Message);
|
||||||
|
return NotFound(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, ex.Message);
|
_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)
|
public async Task<IActionResult> SendRequest(Guid targetUserId)
|
||||||
{
|
{
|
||||||
|
if (targetUserId == Guid.Empty)
|
||||||
|
return BadRequest("Target user ID is invalid");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _friendsService.SendFriendRequestAsync(targetUserId, GetCurrentUserId());
|
await _friendsService.SendFriendRequestAsync(targetUserId, GetCurrentUserId());
|
||||||
return Ok();
|
return Ok(new { message = "Friend request sent successfully." });
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("The user tried to send a friend request to themselves");
|
_logger.LogWarning(ex, "Tried to send request to self");
|
||||||
return BadRequest("You can't send a friend request to themselves");
|
return UnprocessableEntity(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
catch (RequestAlreadySentException ex)
|
catch (RequestAlreadySentException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("The user tried to send a friend request but the request is already sent");
|
_logger.LogWarning(ex, ex.Message);
|
||||||
return BadRequest("You already sent a friend request");
|
return Conflict(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, ex.Message);
|
_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()
|
public async Task<IActionResult> GetIncomingRequests()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Getting incoming requests for user {GetCurrentUserId()}...");
|
|
||||||
var result = await _friendsService.GetIncomingRequestsAsync(GetCurrentUserId());
|
var result = await _friendsService.GetIncomingRequestsAsync(GetCurrentUserId());
|
||||||
return Ok(BuildFriendshipDtos(result));
|
return Ok(BuildFriendshipDtos(result));
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, ex.Message);
|
_logger.LogError(ex, ex.Message);
|
||||||
return BadRequest("You can't get real friend requests because we can't find your user data! Try again");
|
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)
|
public async Task<IActionResult> AcceptFriend(Guid requesterId)
|
||||||
{
|
{
|
||||||
|
if (requesterId == Guid.Empty)
|
||||||
|
return BadRequest("Requester ID is invalid");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _friendsService.AcceptFriendRequestAsync(requesterId, GetCurrentUserId());
|
await _friendsService.AcceptFriendRequestAsync(requesterId, GetCurrentUserId());
|
||||||
return Ok();
|
return Ok(new { message = "Friend request accepted." });
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("The user tried to accept a friend request but the request not exists");
|
_logger.LogWarning(ex, ex.Message);
|
||||||
return BadRequest(ex.Message);
|
return NotFound(new { error = ex.Message });
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("User tried to accept a friend request but addresseeId not equal userId");
|
_logger.LogWarning(ex, ex.Message);
|
||||||
return BadRequest("You can't accept a friend request");
|
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()
|
public async Task<IActionResult> GetFriends()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation($"Getting friends by user {GetCurrentUserId()}...");
|
|
||||||
var result = await _friendsService.GetFriendsAsync(GetCurrentUserId());
|
var result = await _friendsService.GetFriendsAsync(GetCurrentUserId());
|
||||||
|
|
||||||
return Ok(BuildUserDtos(result));
|
return Ok(BuildUserDtos(result));
|
||||||
}
|
}
|
||||||
catch(UnauthorizedAccessException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, ex.Message);
|
_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;
|
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)
|
private List<FriendshipDto> BuildFriendshipDtos(IEnumerable<Friendship> friendships) => friendships.Select(f => new FriendshipDto
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
List<FriendshipDto> dtos = new List<FriendshipDto>();
|
Id = f.Id,
|
||||||
|
Status = f.Status,
|
||||||
foreach (var friendship in result)
|
AddresseeId = f.AddresseeId,
|
||||||
{
|
RequesterId = f.RequesterId
|
||||||
dtos.Add(new FriendshipDto()
|
}).ToList();
|
||||||
{
|
|
||||||
Id = friendship.Id,
|
|
||||||
Status = friendship.Status,
|
|
||||||
AddresseeId = friendship.AddresseeId,
|
|
||||||
RequesterId = friendship.RequesterId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dtos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -59,11 +59,6 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddControllers()
|
|
||||||
.AddJsonOptions(options =>
|
|
||||||
{
|
|
||||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Init DI
|
// Init DI
|
||||||
builder.Services.AddServices();
|
builder.Services.AddServices();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace Govor.Application.Interfaces;
|
|||||||
|
|
||||||
public interface IFriendsService
|
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 SendFriendRequestAsync(Guid fromUserId, Guid toUserId);
|
||||||
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
|
Task AcceptFriendRequestAsync(Guid requestId, Guid currentUserId);
|
||||||
Task<List<User>> GetFriendsAsync(Guid userId);
|
Task<List<User>> GetFriendsAsync(Guid userId);
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ public class FriendsService : IFriendsService
|
|||||||
_friendshipsRepository = relationshipsRepository;
|
_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
|
try
|
||||||
{
|
{
|
||||||
|
all = await _usersRepository.SearchPotentialFriendsAsync(currentId, query);
|
||||||
|
|
||||||
var friends = await _friendshipsRepository.FindByUserIdAsync(currentId);
|
var friends = await _friendshipsRepository.FindByUserIdAsync(currentId);
|
||||||
|
|
||||||
friends = friends.Where(f => f.Status == FriendshipStatus.Accepted).ToList();
|
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))
|
.Where(u => u.Id != currentId && !friends.Select(f => f.RequesterId).Contains(u.Id))
|
||||||
.ToList();
|
.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)
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
{
|
{
|
||||||
return all.Where(u => u.Id != currentId).ToList();
|
return all.Where(u => u.Id != currentId).ToList();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
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)
|
.Where(f => f.Status == FriendshipStatus.Accepted)
|
||||||
.Select(f => f.Addressee);
|
.Select(f => f.RequesterId == userId ? f.Addressee : f.Requester)
|
||||||
|
.ToList();
|
||||||
var received = user.ReceivedFriendRequests
|
|
||||||
.Where(f => f.Status == FriendshipStatus.Accepted)
|
|
||||||
.Select(f => f.Requester);
|
|
||||||
|
|
||||||
return sent.Concat(received).ToList();
|
|
||||||
}
|
}
|
||||||
catch (NotFoundByKeyException<Guid> ex)
|
catch (NotFoundByKeyException<Guid> ex)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("User not exist", ex);
|
throw new InvalidOperationException("User not found", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,21 @@ public class User
|
|||||||
public Invitation? Invite { get; set; }
|
public Invitation? Invite { get; set; }
|
||||||
public List<Friendship> SentFriendRequests { get; set; } = new();
|
public List<Friendship> SentFriendRequests { get; set; } = new();
|
||||||
public List<Friendship> ReceivedFriendRequests { 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ public class UsersRepository : IUsersRepository
|
|||||||
(f.RequesterId == u.Id && f.AddresseeId == currentUserId)))
|
(f.RequesterId == u.Id && f.AddresseeId == currentUserId)))
|
||||||
.Take(20)
|
.Take(20)
|
||||||
.OrderBy(u => u.Username)
|
.OrderBy(u => u.Username)
|
||||||
.ToListAsync();
|
.ToListOrThrowIfEmpty(new NotFoundByKeyException<(string, Guid)>((query, currentUserId), $"Users with given query for user {currentUserId} not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user