From 0481bc049f327171d4b1acf753b7bd637fd90a32 Mon Sep 17 00:00:00 2001 From: KinTrae Date: Sat, 4 Jan 2025 17:24:55 +0100 Subject: [PATCH] feature: User relations API task: 86977vgkk --- .../repositories/RelationTypeRepository.java | 7 - .../repositories/UserRelationRepository.java | 7 - .../shared/constants/AlertConstants.java | 10 +- .../shared/exceptions/RelationException.java | 8 + .../handlers/GlobalExceptionHandler.java | 10 ++ .../backend/shared/utils/AlertUtils.java | 25 +++- .../controllers/UserRelationController.java | 80 ++++++++++ .../models}/RelationType.java | 2 +- .../models}/UserRelation.java | 2 +- .../repositories/RelationTypeRepository.java | 10 ++ .../repositories/UserRelationRepository.java | 130 +++++++++++++++++ .../services/UserRelationQueryService.java | 16 ++ .../services/UserRelationService.java | 11 ++ .../impl/UserRelationQueryServiceImpl.java | 56 +++++++ .../impl/UserRelationServiceImpl.java | 100 +++++++++++++ .../users/controllers/UserController.java | 10 +- .../backend/users/dtos/BasicUserInfoDto.java | 2 +- .../facades/UserRelationServiceFacade.java | 9 ++ .../impl/UserRelationServiceFacadeImpl.java | 25 ++++ .../meowhub/backend/users/models/User.java | 2 +- .../users/repositories/UserRepository.java | 25 ++++ .../users/services/UserQueryService.java | 3 + .../services/impl/UserQueryServiceImpl.java | 13 ++ .../meowhub/backend/InitDataTestConfig.java | 10 +- ...UserRelationControllerIntegrationTest.java | 93 ++++++++++++ .../UserRelationQueryServiceImplTest.java | 137 ++++++++++++++++++ ...UserRelationRepositoryIntegrationTest.java | 95 ++++++++++++ database/scripts/120_create_tables.sql | 3 +- 28 files changed, 872 insertions(+), 29 deletions(-) delete mode 100644 backend/src/main/java/meowhub/backend/repositories/RelationTypeRepository.java delete mode 100644 backend/src/main/java/meowhub/backend/repositories/UserRelationRepository.java create mode 100644 backend/src/main/java/meowhub/backend/shared/exceptions/RelationException.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/controllers/UserRelationController.java rename backend/src/main/java/meowhub/backend/{jpa_buddy => user_relations/models}/RelationType.java (96%) rename backend/src/main/java/meowhub/backend/{jpa_buddy => user_relations/models}/UserRelation.java (97%) create mode 100644 backend/src/main/java/meowhub/backend/user_relations/repositories/RelationTypeRepository.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/repositories/UserRelationRepository.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/services/UserRelationQueryService.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/services/UserRelationService.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationQueryServiceImpl.java create mode 100644 backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationServiceImpl.java create mode 100644 backend/src/main/java/meowhub/backend/users/facades/UserRelationServiceFacade.java create mode 100644 backend/src/main/java/meowhub/backend/users/facades/impl/UserRelationServiceFacadeImpl.java create mode 100644 backend/src/test/java/meowhub/backend/user_relations/UserRelationControllerIntegrationTest.java create mode 100644 backend/src/test/java/meowhub/backend/user_relations/UserRelationQueryServiceImplTest.java create mode 100644 backend/src/test/java/meowhub/backend/user_relations/UserRelationRepositoryIntegrationTest.java diff --git a/backend/src/main/java/meowhub/backend/repositories/RelationTypeRepository.java b/backend/src/main/java/meowhub/backend/repositories/RelationTypeRepository.java deleted file mode 100644 index 964fcbb..0000000 --- a/backend/src/main/java/meowhub/backend/repositories/RelationTypeRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package meowhub.backend.repositories; - -import meowhub.backend.jpa_buddy.RelationType; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RelationTypeRepository extends JpaRepository { -} \ No newline at end of file diff --git a/backend/src/main/java/meowhub/backend/repositories/UserRelationRepository.java b/backend/src/main/java/meowhub/backend/repositories/UserRelationRepository.java deleted file mode 100644 index 56f2659..0000000 --- a/backend/src/main/java/meowhub/backend/repositories/UserRelationRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package meowhub.backend.repositories; - -import meowhub.backend.jpa_buddy.UserRelation; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserRelationRepository extends JpaRepository { -} \ No newline at end of file diff --git a/backend/src/main/java/meowhub/backend/shared/constants/AlertConstants.java b/backend/src/main/java/meowhub/backend/shared/constants/AlertConstants.java index cc6e6ec..6071c15 100644 --- a/backend/src/main/java/meowhub/backend/shared/constants/AlertConstants.java +++ b/backend/src/main/java/meowhub/backend/shared/constants/AlertConstants.java @@ -2,16 +2,24 @@ import lombok.NoArgsConstructor; -@NoArgsConstructor +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class AlertConstants { //title public static final String USER_WITH_LOGIN_NOT_FOUND_TITLE = "User not found"; + + public static final String RELATION_FOR_USERS_NOT_FOUND_TITLE = "Relation not found"; + public static final String RELATION_ALREADY_EXISTS_TITLE = "Relation already exists"; + public static final String RESOURCE_NOT_FOUND_TITLE = "Resource not found"; public static final String NOT_UNIQUE_OBJECT_TITLE = "Not unique object"; public static final String ILLEGAL_ARGUMENT_TITLE = "Illegal argument"; //message public static final String USER_WITH_LOGIN_NOT_FOUND = "User with login '%s' not found"; + + public static final String RELATION_FOR_USERS_NOT_FOUND = "Relation %s not found for users %s and %s"; + public static final String RELATION_ALREADY_EXISTS = "Relation %s already exists for users %s and %s"; + public static final String RESOURCE_NOT_FOUND = "%s not found for %s = '%s'"; public static final String UNKNOWN_ERROR = "Unknown error"; public static final String BAD_CREDENTIALS = "Bad credentials"; diff --git a/backend/src/main/java/meowhub/backend/shared/exceptions/RelationException.java b/backend/src/main/java/meowhub/backend/shared/exceptions/RelationException.java new file mode 100644 index 0000000..f72937b --- /dev/null +++ b/backend/src/main/java/meowhub/backend/shared/exceptions/RelationException.java @@ -0,0 +1,8 @@ +package meowhub.backend.shared.exceptions; + +public class RelationException extends RuntimeException { + + public RelationException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/meowhub/backend/shared/handlers/GlobalExceptionHandler.java b/backend/src/main/java/meowhub/backend/shared/handlers/GlobalExceptionHandler.java index fd5d459..a78ac77 100644 --- a/backend/src/main/java/meowhub/backend/shared/handlers/GlobalExceptionHandler.java +++ b/backend/src/main/java/meowhub/backend/shared/handlers/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import meowhub.backend.shared.dtos.AlertDto; import meowhub.backend.shared.exceptions.NotUniqueObjectException; +import meowhub.backend.shared.exceptions.RelationException; import meowhub.backend.shared.utils.AlertUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -29,6 +30,15 @@ public ResponseEntity handleNotFoundException(NotFoundException ex) { return new ResponseEntity<>(AlertUtils.resourceNotFoundException(ex.getMessage()), HttpStatus.NOT_FOUND); } + @ExceptionHandler(RelationException.class) + public ResponseEntity handleNotFoundException(RelationException ex) { + if(ex.getMessage().contains("exists")) { + return new ResponseEntity<>(AlertUtils.relationAlreadyExists(ex.getMessage()), HttpStatus.CONFLICT); + } else { + return new ResponseEntity<>(AlertUtils.relationNotFoundException(ex.getMessage()), HttpStatus.NOT_FOUND); + } + } + @ExceptionHandler(Exception.class) public ResponseEntity handleUnknownException(Exception ex) { ex.printStackTrace(); diff --git a/backend/src/main/java/meowhub/backend/shared/utils/AlertUtils.java b/backend/src/main/java/meowhub/backend/shared/utils/AlertUtils.java index 0178158..41ebbf6 100644 --- a/backend/src/main/java/meowhub/backend/shared/utils/AlertUtils.java +++ b/backend/src/main/java/meowhub/backend/shared/utils/AlertUtils.java @@ -1,11 +1,13 @@ package meowhub.backend.shared.utils; +import lombok.NoArgsConstructor; import meowhub.backend.shared.constants.AlertConstants; import meowhub.backend.shared.constants.AlertLevel; import meowhub.backend.shared.dtos.AlertDto; import java.time.LocalDateTime; +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class AlertUtils { public static AlertDto userNotFoundException(String msg) { @@ -18,6 +20,26 @@ public static AlertDto userNotFoundException(String msg) { return alertDto; } + public static AlertDto relationNotFoundException(String msg) { + AlertDto alertDto = new AlertDto(); + alertDto.setTitle(AlertConstants.RELATION_FOR_USERS_NOT_FOUND_TITLE); + alertDto.setMessage(msg); + alertDto.setLevel(AlertLevel.ERROR); + alertDto.setTimestamp(LocalDateTime.now()); + + return alertDto; + } + + public static AlertDto relationAlreadyExists(String msg) { + AlertDto alertDto = new AlertDto(); + alertDto.setTitle(AlertConstants.RELATION_ALREADY_EXISTS_TITLE); + alertDto.setMessage(msg); + alertDto.setLevel(AlertLevel.ERROR); + alertDto.setTimestamp(LocalDateTime.now()); + + return alertDto; + } + public static AlertDto unknownException() { AlertDto alertDto = new AlertDto(); alertDto.setTitle(AlertConstants.UNKNOWN_ERROR); @@ -67,7 +89,4 @@ public static AlertDto illegalArgumentException(String msg) { return alertDto; } - - private AlertUtils() { - } } diff --git a/backend/src/main/java/meowhub/backend/user_relations/controllers/UserRelationController.java b/backend/src/main/java/meowhub/backend/user_relations/controllers/UserRelationController.java new file mode 100644 index 0000000..3c189e7 --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/controllers/UserRelationController.java @@ -0,0 +1,80 @@ +package meowhub.backend.user_relations.controllers; + +import lombok.RequiredArgsConstructor; +import meowhub.backend.user_relations.services.UserRelationQueryService; +import meowhub.backend.user_relations.services.UserRelationService; +import meowhub.backend.users.dtos.BasicUserInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("api/relations") +@RequiredArgsConstructor +public class UserRelationController { + private final UserRelationService service; + private final UserRelationQueryService queryService; + + @GetMapping("friends") + public ResponseEntity> getFriends(@AuthenticationPrincipal UserDetails userDetails, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page friends = queryService.getFriends(userDetails.getUsername(), page, size); + return ResponseEntity.ok(friends); + } + + @GetMapping("{login}/friends") + public ResponseEntity> getFriendsForUser(@PathVariable String login, @AuthenticationPrincipal UserDetails userDetails, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page friends = queryService.getFriendsForUser(login, userDetails.getUsername(), page, size); + return ResponseEntity.ok(friends); + } + + @GetMapping("pending") + public ResponseEntity> getPendingSentRequests(@AuthenticationPrincipal UserDetails userDetails, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page pending = queryService.getPendingSentRequests(userDetails.getUsername(), page, size); + return ResponseEntity.ok(pending); + } + + @GetMapping("received") + public ResponseEntity> getReceivedRequests(@AuthenticationPrincipal UserDetails userDetails, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page received = queryService.getReceivedRequests(userDetails.getUsername(), page, size); + return ResponseEntity.ok(received); + } + + @GetMapping("rejected") + public ResponseEntity> getRejectedRequests(@AuthenticationPrincipal UserDetails userDetails, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page rejected = queryService.getRejectedRequests(userDetails.getUsername(), page, size); + return ResponseEntity.ok(rejected); + } + + @PostMapping("{login}/send") + public ResponseEntity sendFriendRequest(@PathVariable String login, @AuthenticationPrincipal UserDetails userDetails) { + service.sendFriendRequestTo(login, userDetails.getUsername()); + return ResponseEntity.ok().build(); + } + + @PostMapping("{login}/reject") + public ResponseEntity rejectFriendRequest(@PathVariable String login, @AuthenticationPrincipal UserDetails userDetails) { + service.rejectFriendRequestFrom(login, userDetails.getUsername()); + return ResponseEntity.ok().build(); + } + + @PostMapping("{acceptFrom}/accept") + public ResponseEntity acceptFriendRequest(@PathVariable String acceptFrom, @AuthenticationPrincipal UserDetails userDetails) { + service.acceptFriendRequestFrom(acceptFrom, userDetails.getUsername()); + return ResponseEntity.ok().build(); + } + + @PostMapping("{login}/delete-friend") + public ResponseEntity deleteFriend(@PathVariable String login, @AuthenticationPrincipal UserDetails userDetails) { + service.deleteFriend(login, userDetails.getUsername()); + return ResponseEntity.ok().build(); + } + + +} diff --git a/backend/src/main/java/meowhub/backend/jpa_buddy/RelationType.java b/backend/src/main/java/meowhub/backend/user_relations/models/RelationType.java similarity index 96% rename from backend/src/main/java/meowhub/backend/jpa_buddy/RelationType.java rename to backend/src/main/java/meowhub/backend/user_relations/models/RelationType.java index a795c51..34ced81 100644 --- a/backend/src/main/java/meowhub/backend/jpa_buddy/RelationType.java +++ b/backend/src/main/java/meowhub/backend/user_relations/models/RelationType.java @@ -1,4 +1,4 @@ -package meowhub.backend.jpa_buddy; +package meowhub.backend.user_relations.models; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/meowhub/backend/jpa_buddy/UserRelation.java b/backend/src/main/java/meowhub/backend/user_relations/models/UserRelation.java similarity index 97% rename from backend/src/main/java/meowhub/backend/jpa_buddy/UserRelation.java rename to backend/src/main/java/meowhub/backend/user_relations/models/UserRelation.java index 65c125a..1e5f3a1 100644 --- a/backend/src/main/java/meowhub/backend/jpa_buddy/UserRelation.java +++ b/backend/src/main/java/meowhub/backend/user_relations/models/UserRelation.java @@ -1,4 +1,4 @@ -package meowhub.backend.jpa_buddy; +package meowhub.backend.user_relations.models; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/backend/src/main/java/meowhub/backend/user_relations/repositories/RelationTypeRepository.java b/backend/src/main/java/meowhub/backend/user_relations/repositories/RelationTypeRepository.java new file mode 100644 index 0000000..dac4030 --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/repositories/RelationTypeRepository.java @@ -0,0 +1,10 @@ +package meowhub.backend.user_relations.repositories; + +import meowhub.backend.user_relations.models.RelationType; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface RelationTypeRepository extends JpaRepository { + Optional findByCode(String code); +} \ No newline at end of file diff --git a/backend/src/main/java/meowhub/backend/user_relations/repositories/UserRelationRepository.java b/backend/src/main/java/meowhub/backend/user_relations/repositories/UserRelationRepository.java new file mode 100644 index 0000000..ea34b1e --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/repositories/UserRelationRepository.java @@ -0,0 +1,130 @@ +package meowhub.backend.user_relations.repositories; + +import meowhub.backend.user_relations.models.UserRelation; +import meowhub.backend.users.dtos.BasicUserInfoDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + + +public interface UserRelationRepository extends JpaRepository { + @Query(value = """ + SELECT new meowhub.backend.users.dtos.BasicUserInfoDto ( + receiver.id, + receiver.name, + receiver.surname, + receiver.login, + null + ) + FROM UserRelation relation + JOIN User sender ON relation.sender.id = sender.id + JOIN RelationType relationType ON relation.relationType.id = relationType.id + JOIN User receiver ON relation.receiver.id = receiver.id + WHERE sender.login = :login + AND relationType.code = :relationTypeCode + """) + Page findRelationsFor(@Param("login") String login, @Param("relationTypeCode") String relationTypeCode, Pageable pageable); + + @Query(value = """ + SELECT new meowhub.backend.users.dtos.BasicUserInfoDto ( + sender.id, + sender.name, + sender.surname, + sender.login, + null + ) + FROM UserRelation relation + JOIN User sender ON relation.sender.id = sender.id + JOIN RelationType relationType ON relation.relationType.id = relationType.id + JOIN User receiver ON relation.receiver.id = receiver.id + WHERE receiver.login = :login + AND relationType.code = :relationTypeCode + """) + Page findRelationsWhereReceiverIs(@Param("login") String receiverLogin, @Param("relationTypeCode") String relationTypeCode, Pageable pageable); + + @Query(value = """ + SELECT new meowhub.backend.users.dtos.BasicUserInfoDto ( + sender.id, + sender.name, + sender.surname, + sender.login, + null + ) + FROM UserRelation relation + JOIN User sender ON relation.sender.id = sender.id + JOIN User receiver ON relation.receiver.id = receiver.id + JOIN RelationType relationType ON relation.relationType.id = relationType.id + WHERE receiver.login = :login + AND relationType.code = 'FRIENDS' + UNION + SELECT new meowhub.backend.users.dtos.BasicUserInfoDto ( + receiver.id, + receiver.name, + receiver.surname, + receiver.login, + null + ) + FROM UserRelation relation + JOIN User sender ON relation.sender.id = sender.id + JOIN User receiver ON relation.receiver.id = receiver.id + JOIN RelationType relationType ON relation.relationType.id = relationType.id + WHERE sender.login = :login + AND relationType.code = 'FRIENDS' + """) + Page findFriendsFor (@Param("login") String login, Pageable pageable); + + + @Modifying + @Query(""" + UPDATE UserRelation relation + SET relation.relationType = (SELECT r FROM RelationType r WHERE r.code = :relationTypeCode), + relation.answerDate = CURRENT_TIMESTAMP + WHERE relation.sender.login = :updateRelationWithLogin AND relation.receiver.login = :currentlyLoggedUserLogin + AND relation.relationType.code = 'SENT_INVITATION' + """) + void updateRelationType(@Param("relationTypeCode") String relationTypeCode, @Param("updateRelationWithLogin") String updateRelationWithLogin, @Param("currentlyLoggedUserLogin") String currentlyLoggedUserLogin); + + + boolean existsUserRelationBySenderLoginAndReceiverLoginAndRelationTypeCode(String senderLogin, String receiverLogin, String relationTypeCode); + + /*** + * Deletes friend relation between two users. The relation is symmetric, so it doesn't matter who is the sender and who is the receiver. + * @param login + * @param requesterLogin + */ + @Modifying + @Query(""" + DELETE + FROM UserRelation relation + WHERE (relation.sender.login = :requesterLogin + AND relation.receiver.login = :login) + OR (relation.receiver.login = :requesterLogin + AND relation.sender.login = :login) + """) + void deleteFriend(@Param("login") String login, @Param("requesterLogin") String requesterLogin); + + @Query(""" + SELECT + CASE WHEN EXISTS ( + SELECT 1 + FROM User requestedUser + WHERE requestedUser.login = :requestedLogin + AND (requestedUser.friendsPrivacy.code = 'PUBLIC' + OR (requestedUser.friendsPrivacy.code = 'FRIENDS' + AND EXISTS ( + SELECT 1 + FROM UserRelation relation + WHERE relation.relationType.code = 'FRIENDS' + AND (relation.sender.login = :requestedLogin AND relation.receiver.login = :requestedBy + OR + relation.sender.login = :requestedBy AND relation.receiver.login = :requestedLogin) + ) + ) + ) + ) THEN true ELSE false END + """) + boolean canViewUserPosts(@Param("requestedLogin") String requestedLogin, @Param("requestedBy") String requestedBy); +} \ No newline at end of file diff --git a/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationQueryService.java b/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationQueryService.java new file mode 100644 index 0000000..b10699f --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationQueryService.java @@ -0,0 +1,16 @@ +package meowhub.backend.user_relations.services; + +import meowhub.backend.users.dtos.BasicUserInfoDto; +import org.springframework.data.domain.Page; + +public interface UserRelationQueryService { + Page getFriends(String login, int page, int size); + + Page getFriendsForUser(String login, String requestedBy, int page, int size); + + Page getPendingSentRequests(String login, int page, int size); + + Page getReceivedRequests(String login, int page, int size); + + Page getRejectedRequests(String login, int page, int size); +} \ No newline at end of file diff --git a/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationService.java b/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationService.java new file mode 100644 index 0000000..db2eb30 --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/services/UserRelationService.java @@ -0,0 +1,11 @@ +package meowhub.backend.user_relations.services; + +public interface UserRelationService { + void acceptFriendRequestFrom(String login, String accepterLogin); + + void sendFriendRequestTo (String login, String senderLogin); + + void rejectFriendRequestFrom(String login, String rejecterLogin); + + void deleteFriend(String login, String requesterLogin); +} diff --git a/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationQueryServiceImpl.java b/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationQueryServiceImpl.java new file mode 100644 index 0000000..943856e --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationQueryServiceImpl.java @@ -0,0 +1,56 @@ +package meowhub.backend.user_relations.services.impl; + +import lombok.AllArgsConstructor; +import meowhub.backend.dtos.RelationType; +import meowhub.backend.shared.exceptions.RelationException; +import meowhub.backend.user_relations.repositories.UserRelationRepository; +import meowhub.backend.user_relations.services.UserRelationQueryService; +import meowhub.backend.users.dtos.BasicUserInfoDto; +import meowhub.backend.users.facades.UserRelationServiceFacade; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@AllArgsConstructor +public class UserRelationQueryServiceImpl implements UserRelationQueryService { + private final UserRelationRepository userRelationRepository; + private final UserRelationServiceFacade userRelationServiceFacade; + + @Override + public Page getFriends(String login, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return userRelationRepository.findFriendsFor(login, pageable); + } + + @Override + public Page getFriendsForUser(String login, String requestedBy, int page, int size) { + userRelationServiceFacade.validateIfUserExists(login); + + if(userRelationRepository.canViewUserPosts(login, requestedBy)) { + Pageable pageable = PageRequest.of(page, size); + return userRelationRepository.findFriendsFor(login, pageable); + } else { + throw new RelationException("User cannot view friends of this user"); + } + } + + @Override + public Page getPendingSentRequests(String login, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return userRelationRepository.findRelationsFor(login, RelationType.SENT_INVITATION.name(), pageable); + } + + @Override + public Page getReceivedRequests(String login, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return userRelationRepository.findRelationsWhereReceiverIs(login, RelationType.SENT_INVITATION.name(), pageable); + } + + @Override + public Page getRejectedRequests(String login, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + return userRelationRepository.findRelationsWhereReceiverIs(login, RelationType.REJECTED.name(), pageable); + } +} diff --git a/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationServiceImpl.java b/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationServiceImpl.java new file mode 100644 index 0000000..6f4dd86 --- /dev/null +++ b/backend/src/main/java/meowhub/backend/user_relations/services/impl/UserRelationServiceImpl.java @@ -0,0 +1,100 @@ +package meowhub.backend.user_relations.services.impl; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import meowhub.backend.dtos.RelationType; +import meowhub.backend.shared.constants.AlertConstants; +import meowhub.backend.shared.exceptions.RelationException; +import meowhub.backend.user_relations.models.UserRelation; +import meowhub.backend.user_relations.repositories.RelationTypeRepository; +import meowhub.backend.user_relations.repositories.UserRelationRepository; +import meowhub.backend.user_relations.services.UserRelationService; +import meowhub.backend.users.facades.UserRelationServiceFacade; +import meowhub.backend.users.models.User; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +@Transactional +public class UserRelationServiceImpl implements UserRelationService { + private final UserRelationRepository userRelationRepository; + private final RelationTypeRepository relationTypeRepository; + private final UserRelationServiceFacade userRelationFacade; + + @Override + public void acceptFriendRequestFrom (String login, String accepterLogin) { + validateSenderAndReceiverExist(login, accepterLogin); + + if(twoWayRelationExists(login, accepterLogin, RelationType.FRIENDS)) { + throw new RelationException(String.format(AlertConstants.RELATION_ALREADY_EXISTS, RelationType.FRIENDS, login, accepterLogin)); + } else if (!oneWayRelationExists(login, accepterLogin, RelationType.SENT_INVITATION)) { + throw new RelationException(String.format(AlertConstants.RELATION_FOR_USERS_NOT_FOUND, RelationType.SENT_INVITATION, login, accepterLogin)); + } + + userRelationRepository.updateRelationType(RelationType.FRIENDS.name(), login, accepterLogin); + } + + @Override + public void sendFriendRequestTo(String login, String senderLogin) { + User sender = userRelationFacade.findUserByLogin(senderLogin); + User receiver = userRelationFacade.findUserByLogin(login); + + //validate if relation already exists + if(twoWayRelationExists(senderLogin, login, RelationType.FRIENDS)) { + throw new RelationException(String.format(AlertConstants.RELATION_ALREADY_EXISTS, RelationType.FRIENDS, senderLogin, login)); + } else if (twoWayRelationExists(senderLogin, login, RelationType.SENT_INVITATION)) { + throw new RelationException(String.format(AlertConstants.RELATION_ALREADY_EXISTS, RelationType.SENT_INVITATION, senderLogin, login)); + } + + UserRelation userRelation = new UserRelation(); + userRelation.setRelationType(relationTypeRepository.findByCode(RelationType.SENT_INVITATION.name()).orElseThrow()); + userRelation.setSender(sender); + userRelation.setReceiver(receiver); + userRelation.setSendDate(LocalDateTime.now()); + userRelationRepository.save(userRelation); + } + + @Override + public void rejectFriendRequestFrom (String login, String rejecterLogin) { + validateSenderAndReceiverExist(login, rejecterLogin); + + //validate if relation already exists + if(twoWayRelationExists(login, rejecterLogin, RelationType.FRIENDS)) { + throw new RelationException(String.format(AlertConstants.RELATION_ALREADY_EXISTS, RelationType.FRIENDS, login, rejecterLogin)); + } else if (!oneWayRelationExists(login, rejecterLogin, RelationType.SENT_INVITATION)) { + throw new RelationException(String.format(AlertConstants.RELATION_FOR_USERS_NOT_FOUND, RelationType.SENT_INVITATION, login, rejecterLogin)); + } + + userRelationRepository.updateRelationType(RelationType.REJECTED.name(), login, rejecterLogin); + } + + @Override + public void deleteFriend(String login, String requesterLogin) { + validateSenderAndReceiverExist(login, requesterLogin); + + //validate if relation friends exists + if(!twoWayRelationExists(login, requesterLogin, RelationType.FRIENDS)) { + throw new RelationException(String.format(AlertConstants.RELATION_FOR_USERS_NOT_FOUND, RelationType.FRIENDS, login, requesterLogin)); + } + + userRelationRepository.deleteFriend(login, requesterLogin); + } + + private void validateSenderAndReceiverExist(String senderLogin, String receiverLogin) { + userRelationFacade.validateIfUserExists(senderLogin); + userRelationFacade.validateIfUserExists(receiverLogin); + if(senderLogin.equals(receiverLogin)) { + throw new RelationException(String.format(AlertConstants.ILLEGAL_ARGUMENT, "Sender", "Receiver")); + } + } + + private boolean twoWayRelationExists(String senderLogin, String receiverLogin, RelationType relationType) { + return oneWayRelationExists(senderLogin, receiverLogin, relationType) || oneWayRelationExists(receiverLogin, senderLogin, relationType); + } + + private boolean oneWayRelationExists (String senderLogin, String receiverLogin, RelationType relationType) { + return userRelationRepository.existsUserRelationBySenderLoginAndReceiverLoginAndRelationTypeCode(senderLogin, receiverLogin, relationType.name()); + } +} diff --git a/backend/src/main/java/meowhub/backend/users/controllers/UserController.java b/backend/src/main/java/meowhub/backend/users/controllers/UserController.java index 38da40a..719cf20 100644 --- a/backend/src/main/java/meowhub/backend/users/controllers/UserController.java +++ b/backend/src/main/java/meowhub/backend/users/controllers/UserController.java @@ -3,9 +3,11 @@ import lombok.RequiredArgsConstructor; import meowhub.backend.users.dtos.BasicUserInfoDto; import meowhub.backend.users.services.UserQueryService; +import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -14,8 +16,14 @@ public class UserController { private final UserQueryService userQueryService; - @GetMapping("basic-user-info") + @GetMapping("/basic-user-info") public ResponseEntity getBasicUserInfoTemp(String login) { return ResponseEntity.ok(userQueryService.getBasicUserInfo(login)); } + + @GetMapping("/search") + public ResponseEntity> searchUsers(@RequestParam(required = false) String query, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { + Page users = userQueryService.searchUsers(query, page, size); + return ResponseEntity.ok(users); + } } diff --git a/backend/src/main/java/meowhub/backend/users/dtos/BasicUserInfoDto.java b/backend/src/main/java/meowhub/backend/users/dtos/BasicUserInfoDto.java index f10cd00..52b7197 100644 --- a/backend/src/main/java/meowhub/backend/users/dtos/BasicUserInfoDto.java +++ b/backend/src/main/java/meowhub/backend/users/dtos/BasicUserInfoDto.java @@ -14,5 +14,5 @@ public class BasicUserInfoDto { private String name; private String surname; private String login; - private byte[] profilePicture; + private byte[] profilePicture; //TODO: add picture functionality } diff --git a/backend/src/main/java/meowhub/backend/users/facades/UserRelationServiceFacade.java b/backend/src/main/java/meowhub/backend/users/facades/UserRelationServiceFacade.java new file mode 100644 index 0000000..d4a973a --- /dev/null +++ b/backend/src/main/java/meowhub/backend/users/facades/UserRelationServiceFacade.java @@ -0,0 +1,9 @@ +package meowhub.backend.users.facades; + +import meowhub.backend.users.models.User; + +public interface UserRelationServiceFacade { + User findUserByLogin(String login); + + void validateIfUserExists(String login); +} diff --git a/backend/src/main/java/meowhub/backend/users/facades/impl/UserRelationServiceFacadeImpl.java b/backend/src/main/java/meowhub/backend/users/facades/impl/UserRelationServiceFacadeImpl.java new file mode 100644 index 0000000..9cf2f77 --- /dev/null +++ b/backend/src/main/java/meowhub/backend/users/facades/impl/UserRelationServiceFacadeImpl.java @@ -0,0 +1,25 @@ +package meowhub.backend.users.facades.impl; + +import lombok.RequiredArgsConstructor; +import meowhub.backend.users.facades.UserRelationServiceFacade; +import meowhub.backend.users.models.User; +import meowhub.backend.users.services.UserQueryService; +import meowhub.backend.users.services.UserValidationService; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserRelationServiceFacadeImpl implements UserRelationServiceFacade { + private final UserQueryService userQueryService; + private final UserValidationService userValidationService; + + @Override + public User findUserByLogin(String login) { + return userQueryService.findUserByLogin(login); + } + + @Override + public void validateIfUserExists(String login) { + userValidationService.validateIfUserExists(login); + } +} diff --git a/backend/src/main/java/meowhub/backend/users/models/User.java b/backend/src/main/java/meowhub/backend/users/models/User.java index 86f6a25..931a48a 100644 --- a/backend/src/main/java/meowhub/backend/users/models/User.java +++ b/backend/src/main/java/meowhub/backend/users/models/User.java @@ -26,7 +26,7 @@ import meowhub.backend.posts.models.Post; import meowhub.backend.jpa_buddy.Profile; import meowhub.backend.jpa_buddy.UserGroup; -import meowhub.backend.jpa_buddy.UserRelation; +import meowhub.backend.user_relations.models.UserRelation; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; diff --git a/backend/src/main/java/meowhub/backend/users/repositories/UserRepository.java b/backend/src/main/java/meowhub/backend/users/repositories/UserRepository.java index 0b4a866..56632ea 100644 --- a/backend/src/main/java/meowhub/backend/users/repositories/UserRepository.java +++ b/backend/src/main/java/meowhub/backend/users/repositories/UserRepository.java @@ -2,6 +2,8 @@ import meowhub.backend.users.dtos.BasicUserInfoDto; import meowhub.backend.users.models.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -33,4 +35,27 @@ public interface UserRepository extends JpaRepository { FETCH FIRST 1 ROWS ONLY """) Optional findBasicUserInfoByLogin(@Param("login") String login); + + + @Query(""" + SELECT new meowhub.backend.users.dtos.BasicUserInfoDto ( + u.id, + u.name, + u.surname, + u.login, + null + ) + FROM User u + WHERE LOWER(u.login) LIKE LOWER(CONCAT('%', :query, '%')) + OR LOWER(u.name) LIKE LOWER(CONCAT('%', :query, '%')) + OR LOWER(u.surname) LIKE LOWER(CONCAT('%', :query, '%')) + ORDER BY + CASE + WHEN LOWER(u.login) LIKE LOWER(CONCAT('%', :query, '%')) THEN 1 + WHEN LOWER(u.name) LIKE LOWER(CONCAT('%', :query, '%')) THEN 2 + WHEN LOWER(u.surname) LIKE LOWER(CONCAT('%', :query, '%')) THEN 3 + ELSE 4 + END + """) + Page searchByQuery(@Param("query") String query, Pageable pageable); } diff --git a/backend/src/main/java/meowhub/backend/users/services/UserQueryService.java b/backend/src/main/java/meowhub/backend/users/services/UserQueryService.java index 6fbed04..686c5cf 100644 --- a/backend/src/main/java/meowhub/backend/users/services/UserQueryService.java +++ b/backend/src/main/java/meowhub/backend/users/services/UserQueryService.java @@ -3,10 +3,13 @@ import meowhub.backend.dtos.UserDto; import meowhub.backend.users.dtos.BasicUserInfoDto; import meowhub.backend.users.models.User; +import org.springframework.data.domain.Page; import java.util.List; public interface UserQueryService { + Page searchUsers(String query, int page, int size); + List getAllUsers(); BasicUserInfoDto getBasicUserInfo(String login); diff --git a/backend/src/main/java/meowhub/backend/users/services/impl/UserQueryServiceImpl.java b/backend/src/main/java/meowhub/backend/users/services/impl/UserQueryServiceImpl.java index d1d37aa..8ea3e72 100644 --- a/backend/src/main/java/meowhub/backend/users/services/impl/UserQueryServiceImpl.java +++ b/backend/src/main/java/meowhub/backend/users/services/impl/UserQueryServiceImpl.java @@ -10,6 +10,9 @@ import meowhub.backend.users.models.User; import meowhub.backend.users.repositories.UserRepository; import meowhub.backend.users.services.UserQueryService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @@ -20,6 +23,16 @@ public class UserQueryServiceImpl implements UserQueryService { private final UserRepository userRepository; + public Page searchUsers(String query, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + + if (query == null || query.isBlank()) { + return userRepository.findAll(pageable).map(user -> new BasicUserInfoDto(user.getId(), user.getName(), user.getSurname(), user.getLogin(), null)); + } + + return userRepository.searchByQuery(query, pageable); + } + @Override public BasicUserInfoDto getBasicUserInfo(String login) { return userRepository.findBasicUserInfoByLogin(login) diff --git a/backend/src/test/java/meowhub/backend/InitDataTestConfig.java b/backend/src/test/java/meowhub/backend/InitDataTestConfig.java index 3242f18..c0777b9 100644 --- a/backend/src/test/java/meowhub/backend/InitDataTestConfig.java +++ b/backend/src/test/java/meowhub/backend/InitDataTestConfig.java @@ -12,16 +12,16 @@ import meowhub.backend.jpa_buddy.ProfileData; import meowhub.backend.jpa_buddy.ProfilePicture; import meowhub.backend.jpa_buddy.ProfileUserData; -import meowhub.backend.jpa_buddy.RelationType; -import meowhub.backend.jpa_buddy.UserRelation; +import meowhub.backend.user_relations.models.RelationType; +import meowhub.backend.user_relations.models.UserRelation; import meowhub.backend.posts.repositories.CommentRepository; import meowhub.backend.posts.repositories.PostRepository; import meowhub.backend.repositories.ProfileDataRepository; import meowhub.backend.repositories.ProfilePictureRepository; import meowhub.backend.repositories.ProfileRepository; import meowhub.backend.repositories.ProfileUserDataRepository; -import meowhub.backend.repositories.RelationTypeRepository; -import meowhub.backend.repositories.UserRelationRepository; +import meowhub.backend.user_relations.repositories.RelationTypeRepository; +import meowhub.backend.user_relations.repositories.UserRelationRepository; import meowhub.backend.users.models.Gender; import meowhub.backend.users.models.PrivacySetting; import meowhub.backend.users.models.Role; @@ -99,7 +99,7 @@ private void initUser() { user1.setCredentialsExpiryDate(LocalDateTime.now().plusYears(1)); user1.setRole(role); user1.setPostsPrivacy(publicSetting); - user1.setFriendsPrivacy(publicSetting); + user1.setFriendsPrivacy(friendsOnlySetting); user1.setProfilePrivacy(publicSetting); user1.setGender(gender); user1 = userRepository.save(user1); diff --git a/backend/src/test/java/meowhub/backend/user_relations/UserRelationControllerIntegrationTest.java b/backend/src/test/java/meowhub/backend/user_relations/UserRelationControllerIntegrationTest.java new file mode 100644 index 0000000..aa4463d --- /dev/null +++ b/backend/src/test/java/meowhub/backend/user_relations/UserRelationControllerIntegrationTest.java @@ -0,0 +1,93 @@ +package meowhub.backend.user_relations; + +import meowhub.backend.shared.constants.AlertConstants; +import meowhub.backend.shared.exceptions.RelationException; +import meowhub.backend.user_relations.services.UserRelationQueryService; +import meowhub.backend.user_relations.services.UserRelationService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class UserRelationControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserRelationService userRelationService; + + @MockBean + private UserRelationQueryService userRelationQueryService; + + @Test + @WithMockUser + void testUsernameNotFoundException() throws Exception { + String login = "nonexistentUser"; + + Mockito.doThrow(new UsernameNotFoundException(String.format(AlertConstants.USER_WITH_LOGIN_NOT_FOUND, login))) + .when(userRelationService).sendFriendRequestTo(Mockito.eq(login), Mockito.anyString()); + + mockMvc.perform(post("/api/relations/{login}/send", login).with(csrf())) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.title").value(AlertConstants.USER_WITH_LOGIN_NOT_FOUND_TITLE)) + .andExpect(jsonPath("$.message").value(String.format(AlertConstants.USER_WITH_LOGIN_NOT_FOUND, login))) + .andExpect(jsonPath("$.level").value("ERROR")); + } + + @Test + @WithMockUser + void testRelationNotAllowedException() throws Exception { + String login = "restrictedUser"; + + Mockito.doThrow(new RelationException(AlertConstants.RELATION_ALREADY_EXISTS)) + .when(userRelationService).sendFriendRequestTo(Mockito.eq(login), Mockito.anyString()); + + mockMvc.perform(post("/api/relations/{login}/send", login).with(csrf())) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.title").value(AlertConstants.RELATION_ALREADY_EXISTS_TITLE)) + .andExpect(jsonPath("$.message").value(AlertConstants.RELATION_ALREADY_EXISTS)) + .andExpect(jsonPath("$.level").value("ERROR")); + } + + @Test + @WithMockUser + void testGetFriendsForUser_ShouldReturnEmptyList() throws Exception { + String login = "validUser"; + + when(userRelationQueryService.getFriendsForUser(Mockito.eq(login), Mockito.anyString(), Mockito.anyInt(), Mockito.anyInt())) + .thenReturn(Page.empty()); + + mockMvc.perform(get("/api/relations/{login}/friends", login).param("page", "0").param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").isEmpty()); + } + + @Test + @WithMockUser + void testDeleteFriend_ShouldReturnOk() throws Exception { + String login = "friendToDelete"; + + Mockito.doNothing().when(userRelationService).deleteFriend(Mockito.eq(login), Mockito.anyString()); + + mockMvc.perform(post("/api/relations/{login}/delete-friend", login).with(csrf())) + .andExpect(status().isOk()); + } +} + diff --git a/backend/src/test/java/meowhub/backend/user_relations/UserRelationQueryServiceImplTest.java b/backend/src/test/java/meowhub/backend/user_relations/UserRelationQueryServiceImplTest.java new file mode 100644 index 0000000..119ebbe --- /dev/null +++ b/backend/src/test/java/meowhub/backend/user_relations/UserRelationQueryServiceImplTest.java @@ -0,0 +1,137 @@ +package meowhub.backend.user_relations; + +import meowhub.backend.shared.exceptions.RelationException; +import meowhub.backend.user_relations.repositories.UserRelationRepository; +import meowhub.backend.user_relations.services.impl.UserRelationQueryServiceImpl; +import meowhub.backend.users.dtos.BasicUserInfoDto; +import meowhub.backend.users.facades.UserRelationServiceFacade; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class UserRelationQueryServiceImplTest { + + @Mock + private UserRelationRepository userRelationRepository; + + @Mock + private UserRelationServiceFacade userRelationServiceFacade; + + @InjectMocks + private UserRelationQueryServiceImpl userRelationQueryService; + + private static final String VALID_LOGIN = "user"; + private static final String OTHER_USER = "requestedUser"; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void getFriends_ShouldReturnFriends() { + // Given + PageRequest pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(Collections.emptyList()); + when(userRelationRepository.findFriendsFor(VALID_LOGIN, pageable)).thenReturn(mockPage); + + // When + Page result = userRelationQueryService.getFriends(VALID_LOGIN, 0, 10); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(userRelationRepository, times(1)).findFriendsFor(VALID_LOGIN, pageable); + } + + @Test + void getFriendsForUser_ShouldReturnFriendsForUserWhenPermitted() { + // Given + PageRequest pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(Collections.emptyList()); + when(userRelationRepository.canViewUserPosts(OTHER_USER, VALID_LOGIN)).thenReturn(true); + when(userRelationRepository.findFriendsFor(OTHER_USER, pageable)).thenReturn(mockPage); + + // When + Page result = userRelationQueryService.getFriendsForUser(OTHER_USER, VALID_LOGIN, 0, 10); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(userRelationServiceFacade, times(1)).validateIfUserExists(OTHER_USER); + verify(userRelationRepository, times(1)).canViewUserPosts(OTHER_USER, VALID_LOGIN); + verify(userRelationRepository, times(1)).findFriendsFor(OTHER_USER, pageable); + } + + @Test + void getFriendsForUser_ShouldThrowExceptionWhenNotPermitted() { + // Given + when(userRelationRepository.canViewUserPosts(OTHER_USER, VALID_LOGIN)).thenReturn(false); + + // When & Assert + assertThrows(RelationException.class, () -> + userRelationQueryService.getFriendsForUser(OTHER_USER, VALID_LOGIN, 0, 10)); + verify(userRelationServiceFacade, times(1)).validateIfUserExists(OTHER_USER); + verify(userRelationRepository, times(1)).canViewUserPosts(OTHER_USER, VALID_LOGIN); + verify(userRelationRepository, never()).findFriendsFor(anyString(), any()); + } + + @Test + void getPendingSentRequests_ShouldReturnPendingRequests() { + // Given + PageRequest pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(Collections.emptyList()); + when(userRelationRepository.findRelationsFor(VALID_LOGIN, "SENT_INVITATION", pageable)).thenReturn(mockPage); + + // When + Page result = userRelationQueryService.getPendingSentRequests(VALID_LOGIN, 0, 10); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(userRelationRepository, times(1)).findRelationsFor(VALID_LOGIN, "SENT_INVITATION", pageable); + } + + @Test + void getReceivedRequests_ShouldReturnReceivedRequests() { + // Given + PageRequest pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(Collections.emptyList()); + when(userRelationRepository.findRelationsWhereReceiverIs(VALID_LOGIN, "SENT_INVITATION", pageable)).thenReturn(mockPage); + + // When + Page result = userRelationQueryService.getReceivedRequests(VALID_LOGIN, 0, 10); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(userRelationRepository, times(1)).findRelationsWhereReceiverIs(VALID_LOGIN, "SENT_INVITATION", pageable); + } + + @Test + void getRejectedRequests_ShouldReturnRejectedRequests() { + // Given + PageRequest pageable = PageRequest.of(0, 10); + Page mockPage = new PageImpl<>(Collections.emptyList()); + when(userRelationRepository.findRelationsWhereReceiverIs(VALID_LOGIN, "REJECTED", pageable)).thenReturn(mockPage); + + // When + Page result = userRelationQueryService.getRejectedRequests(VALID_LOGIN, 0, 10); + + // Then + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(userRelationRepository, times(1)).findRelationsWhereReceiverIs(VALID_LOGIN, "REJECTED", pageable); + } +} + diff --git a/backend/src/test/java/meowhub/backend/user_relations/UserRelationRepositoryIntegrationTest.java b/backend/src/test/java/meowhub/backend/user_relations/UserRelationRepositoryIntegrationTest.java new file mode 100644 index 0000000..34d885a --- /dev/null +++ b/backend/src/test/java/meowhub/backend/user_relations/UserRelationRepositoryIntegrationTest.java @@ -0,0 +1,95 @@ +package meowhub.backend.user_relations; + +import meowhub.backend.InitDataTestConfig; +import meowhub.backend.user_relations.repositories.UserRelationRepository; +import meowhub.backend.users.dtos.BasicUserInfoDto; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DataJpaTest +@ActiveProfiles("test") +@Import(InitDataTestConfig.class) +class UserRelationRepositoryIntegrationTest { + @Autowired + private UserRelationRepository userRelationRepository; + + private static final String USER_LOGIN = "user"; + private static final String FRIEND_LOGIN = "grzeĊ›"; + private static final String NON_FRIEND_LOGIN = "admin"; + private static final Pageable pageable = PageRequest.of(0, 10); + + @Test + void testFindRelationsFor() { + Page sentInvitations = userRelationRepository.findRelationsFor("admin", "SENT_INVITATION", pageable); + assertNotNull(sentInvitations); + assertEquals(1, sentInvitations.getContent().size()); + assertEquals(USER_LOGIN, sentInvitations.getContent().get(0).getLogin()); + } + + @Test + void testFindRelationsWhereReceiverIs() { + Page receivedInvitations = userRelationRepository.findRelationsWhereReceiverIs(USER_LOGIN, "SENT_INVITATION", pageable); + assertNotNull(receivedInvitations); + assertEquals(1, receivedInvitations.getContent().size()); + assertEquals("admin", receivedInvitations.getContent().get(0).getLogin()); + } + + @Test + void testFindFriendsFor() { + Page friends = userRelationRepository.findFriendsFor(FRIEND_LOGIN, pageable); + assertNotNull(friends); + assertEquals(1, friends.getContent().size()); + assertEquals(USER_LOGIN, friends.getContent().get(0).getLogin()); + } + + @Test + void testCanViewUserPosts_PublicPrivacy() { + boolean canView = userRelationRepository.canViewUserPosts(USER_LOGIN, NON_FRIEND_LOGIN); + assertEquals(true, canView); // User with public privacy settings is viewable by others. + } + + @Test + void testCanViewUserPosts_FriendsOnlyPrivacy_NoFriendship() { + boolean canView = userRelationRepository.canViewUserPosts("admin", USER_LOGIN); + assertEquals(false, canView); // User with friends-only privacy should not be viewable by non-friends. + } + + @Test + void testCanViewUserPosts_FriendsOnlyPrivacy_WithFriendship() { + boolean canView = userRelationRepository.canViewUserPosts(USER_LOGIN, FRIEND_LOGIN); + assertEquals(true, canView); // Friends should be able to view each other's posts. + } + + @Test + void testDeleteFriend() { + // Before deletion, verify the relation exists + Page friends = userRelationRepository.findFriendsFor(FRIEND_LOGIN, pageable); + assertEquals(1, friends.getContent().size()); + + // Perform deletion + userRelationRepository.deleteFriend(USER_LOGIN, FRIEND_LOGIN); + + // Verify the relation is removed + friends = userRelationRepository.findFriendsFor(FRIEND_LOGIN, pageable); + assertEquals(0, friends.getContent().size()); + } + + @Test + void testExistsUserRelationBySenderLoginAndReceiverLoginAndRelationTypeCode() { + boolean exists = userRelationRepository.existsUserRelationBySenderLoginAndReceiverLoginAndRelationTypeCode("admin", USER_LOGIN, "SENT_INVITATION"); + assertEquals(true, exists); + + exists = userRelationRepository.existsUserRelationBySenderLoginAndReceiverLoginAndRelationTypeCode("user", "admin", "FRIENDS"); + assertEquals(false, exists); + } +} + diff --git a/database/scripts/120_create_tables.sql b/database/scripts/120_create_tables.sql index 771686e..2cd5f3b 100644 --- a/database/scripts/120_create_tables.sql +++ b/database/scripts/120_create_tables.sql @@ -364,7 +364,8 @@ CREATE TABLE mh_user_relations.User_Relations created_by varchar2(36) NOT NULL, modified_at date NULL, modified_by varchar2(36) NULL, - CONSTRAINT User_Relations_pk PRIMARY KEY (id) + CONSTRAINT User_Relations_pk PRIMARY KEY (id), + CONSTRAINT UQ_Sender_Receiver UNIQUE (sender_id, receiver_id) ); -- Table: Relation_Types