From 6cdbd044305e2e28a76bfb0e37f59cc447c1eca6 Mon Sep 17 00:00:00 2001 From: szymongrabski Date: Mon, 9 Jun 2025 14:53:07 +0200 Subject: [PATCH 1/3] add auth if is group member or group owner, add group chat controller tests, refactor tests --- .../controller/ActivityController.java | 14 +- .../backend/controller/GroupController.java | 10 +- .../controller/GroupMembershipController.java | 12 +- .../groupchat/GroupChatController.java | 16 ++- ...reShareResponseEntityExceptionHandler.java | 6 + .../exception/UserIsNotGroupOwner.java | 7 + .../fare/backend/service/ActivityService.java | 21 +-- .../service/GroupMembershipService.java | 13 +- .../fare/backend/service/GroupService.java | 13 +- .../fare/backend/service/SecurityService.java | 42 ++++++ .../controller/GroupChatControllerTest.java | 121 ++++++++++++++++++ .../controller/GroupControllerTest.java | 14 +- .../GroupMembershipControllerTest.java | 31 +++-- .../backend/service/ActivityServiceTest.java | 43 ++++--- .../service/GroupMembershipServiceTest.java | 83 ++++++------ .../backend/service/GroupServiceTest.java | 21 +-- 16 files changed, 362 insertions(+), 105 deletions(-) create mode 100644 backend/src/main/java/share/fare/backend/exception/UserIsNotGroupOwner.java create mode 100644 backend/src/main/java/share/fare/backend/service/SecurityService.java create mode 100644 backend/src/test/java/share/fare/backend/controller/GroupChatControllerTest.java diff --git a/backend/src/main/java/share/fare/backend/controller/ActivityController.java b/backend/src/main/java/share/fare/backend/controller/ActivityController.java index 8bc936a..672c3a4 100644 --- a/backend/src/main/java/share/fare/backend/controller/ActivityController.java +++ b/backend/src/main/java/share/fare/backend/controller/ActivityController.java @@ -14,6 +14,7 @@ import share.fare.backend.dto.response.ActivityResponse; import share.fare.backend.entity.User; import share.fare.backend.service.ActivityService; +import share.fare.backend.service.SecurityService; import share.fare.backend.util.PaginatedResponse; @RestController @@ -21,13 +22,14 @@ @RequiredArgsConstructor public class ActivityController { private final ActivityService activityService; + private final SecurityService securityService; @PostMapping public ResponseEntity createActivity( @PathVariable Long groupId, @Valid @RequestBody ActivityRequest request, @AuthenticationPrincipal User user) { - ActivityResponse response = activityService.createActivity(request, user.getId(), groupId); + ActivityResponse response = activityService.createActivity(request, user, groupId); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @@ -35,16 +37,18 @@ public ResponseEntity createActivity( public ResponseEntity updateActivity( @PathVariable Long groupId, @PathVariable Long activityId, - @Valid @RequestBody ActivityRequest request) { - ActivityResponse response = activityService.updateActivity(request, activityId); + @Valid @RequestBody ActivityRequest request, + @AuthenticationPrincipal User user) { + ActivityResponse response = activityService.updateActivity(request, activityId, user); return ResponseEntity.ok(response); } @DeleteMapping("/{activityId}") public ResponseEntity deleteActivity( @PathVariable Long groupId, - @PathVariable Long activityId) { - activityService.deleteActivity(activityId); + @PathVariable Long activityId, + @AuthenticationPrincipal User user) { + activityService.deleteActivity(activityId, user); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/share/fare/backend/controller/GroupController.java b/backend/src/main/java/share/fare/backend/controller/GroupController.java index c230adc..34c55d0 100644 --- a/backend/src/main/java/share/fare/backend/controller/GroupController.java +++ b/backend/src/main/java/share/fare/backend/controller/GroupController.java @@ -61,13 +61,15 @@ public ResponseEntity> getAllGroups( @PutMapping("/{groupId}") public ResponseEntity updateGroup( @PathVariable Long groupId, - @Valid @RequestBody GroupRequest groupRequest) { - return ResponseEntity.ok(groupService.updateGroup(groupId, groupRequest)); + @Valid @RequestBody GroupRequest groupRequest, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok(groupService.updateGroup(groupId, groupRequest, user)); } @DeleteMapping("/{groupId}") - public ResponseEntity deleteGroup(@PathVariable Long groupId) { - groupService.deleteGroup(groupId); + public ResponseEntity deleteGroup(@PathVariable Long groupId, + @AuthenticationPrincipal User user) { + groupService.deleteGroup(groupId, user); return ResponseEntity.noContent().build(); } } \ No newline at end of file diff --git a/backend/src/main/java/share/fare/backend/controller/GroupMembershipController.java b/backend/src/main/java/share/fare/backend/controller/GroupMembershipController.java index eb81510..a6ccf93 100644 --- a/backend/src/main/java/share/fare/backend/controller/GroupMembershipController.java +++ b/backend/src/main/java/share/fare/backend/controller/GroupMembershipController.java @@ -24,15 +24,16 @@ public ResponseEntity addMember( @RequestParam Long userId, @RequestParam(defaultValue = "MEMBER") GroupRole role, @AuthenticationPrincipal User currentUser) { - GroupMembershipResponse response = groupMembershipService.addMemberToGroup(groupId, userId, role, currentUser.getId()); + GroupMembershipResponse response = groupMembershipService.addMemberToGroup(groupId, userId, role, currentUser); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @DeleteMapping("/{userId}") public ResponseEntity removeMember( @PathVariable Long groupId, - @PathVariable Long userId) { - groupMembershipService.removeMemberFromGroup(groupId, userId); + @PathVariable Long userId, + @AuthenticationPrincipal User currentUser) { + groupMembershipService.removeMemberFromGroup(groupId, userId, currentUser); return ResponseEntity.noContent().build(); } @@ -40,8 +41,9 @@ public ResponseEntity removeMember( public ResponseEntity updateMemberRole( @PathVariable Long groupId, @PathVariable Long userId, - @RequestParam GroupRole role) { - GroupMembershipResponse response = groupMembershipService.updateMemberRole(groupId, userId, role); + @RequestParam GroupRole role, + @AuthenticationPrincipal User currentUser) { + GroupMembershipResponse response = groupMembershipService.updateMemberRole(groupId, userId, role, currentUser); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/share/fare/backend/controller/groupchat/GroupChatController.java b/backend/src/main/java/share/fare/backend/controller/groupchat/GroupChatController.java index 7d69135..75da4db 100644 --- a/backend/src/main/java/share/fare/backend/controller/groupchat/GroupChatController.java +++ b/backend/src/main/java/share/fare/backend/controller/groupchat/GroupChatController.java @@ -3,9 +3,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import share.fare.backend.entity.ChatMessage; +import share.fare.backend.entity.Group; +import share.fare.backend.entity.User; +import share.fare.backend.exception.GroupNotFoundException; import share.fare.backend.repository.ChatMessageRepository; +import share.fare.backend.repository.GroupRepository; +import share.fare.backend.service.SecurityService; @RestController @RequestMapping("/api/v1/group") @@ -13,13 +19,21 @@ public class GroupChatController { private final ChatMessageRepository chatMessageRepository; + private final GroupRepository groupRepository; + private final SecurityService securityService; @GetMapping("/{groupId}/chat") public Page getRecentMessages( @PathVariable Long groupId, @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size + @RequestParam(defaultValue = "20") int size, + @AuthenticationPrincipal User user ) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + + securityService.checkIfUserIsInGroup(user, group); + return chatMessageRepository.findByGroupIdOrderByTimestampDesc(groupId, PageRequest.of(page, size)); } } \ No newline at end of file diff --git a/backend/src/main/java/share/fare/backend/exception/FareShareResponseEntityExceptionHandler.java b/backend/src/main/java/share/fare/backend/exception/FareShareResponseEntityExceptionHandler.java index 667dc45..90273b3 100644 --- a/backend/src/main/java/share/fare/backend/exception/FareShareResponseEntityExceptionHandler.java +++ b/backend/src/main/java/share/fare/backend/exception/FareShareResponseEntityExceptionHandler.java @@ -72,6 +72,12 @@ public final ResponseEntity userIsNotInGroupException(Exception ex return new ResponseEntity<>(errorDetails, HttpStatus.CONFLICT); } + @ExceptionHandler(UserIsNotGroupOwner.class) + public final ResponseEntity userIsNotInGroupOwnerException(Exception ex, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false), ex.getStackTrace().length); + return new ResponseEntity<>(errorDetails, HttpStatus.FORBIDDEN); + } + @ExceptionHandler(OwnerDeletionException.class) public final ResponseEntity ownerDeletionException(Exception ex, WebRequest request) { ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false), ex.getStackTrace().length); diff --git a/backend/src/main/java/share/fare/backend/exception/UserIsNotGroupOwner.java b/backend/src/main/java/share/fare/backend/exception/UserIsNotGroupOwner.java new file mode 100644 index 0000000..74d7c0a --- /dev/null +++ b/backend/src/main/java/share/fare/backend/exception/UserIsNotGroupOwner.java @@ -0,0 +1,7 @@ +package share.fare.backend.exception; + +public class UserIsNotGroupOwner extends RuntimeException { + public UserIsNotGroupOwner(Long userId, Long groupId) { + super("User " + userId + " is not group owner of " + groupId); + } +} diff --git a/backend/src/main/java/share/fare/backend/service/ActivityService.java b/backend/src/main/java/share/fare/backend/service/ActivityService.java index 7eb96e0..14e70e4 100644 --- a/backend/src/main/java/share/fare/backend/service/ActivityService.java +++ b/backend/src/main/java/share/fare/backend/service/ActivityService.java @@ -11,20 +11,18 @@ import share.fare.backend.entity.User; import share.fare.backend.exception.ActivityNotFoundException; import share.fare.backend.exception.GroupNotFoundException; -import share.fare.backend.exception.UserNotFoundException; import share.fare.backend.mapper.ActivityMapper; import share.fare.backend.repository.ActivityRepository; import share.fare.backend.repository.GroupRepository; -import share.fare.backend.repository.UserRepository; @Service @RequiredArgsConstructor public class ActivityService { private final ActivityRepository activityRepository; private final GroupRepository groupRepository; - private final UserRepository userRepository; + private final SecurityService securityService; - public ActivityResponse createActivity(ActivityRequest activityRequest, Long createdByUserId, Long groupId) { + public ActivityResponse createActivity(ActivityRequest activityRequest, User currentUser, Long groupId) { if (activityRequest.getStartDate() != null && activityRequest.getEndDate() != null) { if (activityRequest.getStartDate().isAfter(activityRequest.getEndDate())) { throw new IllegalArgumentException("Start date must be before end date."); @@ -35,20 +33,20 @@ public ActivityResponse createActivity(ActivityRequest activityRequest, Long cre .findById(groupId) .orElseThrow(() -> new GroupNotFoundException(groupId)); - User user = userRepository - .findById(createdByUserId) - .orElseThrow(() -> new UserNotFoundException(createdByUserId)); + securityService.checkIfUserIsInGroup(currentUser, group); Activity activity = ActivityMapper.toEntity(activityRequest, group); return ActivityMapper.toResponse(activityRepository.save(activity)); } - public ActivityResponse updateActivity(ActivityRequest request, Long activityId) { + public ActivityResponse updateActivity(ActivityRequest request, Long activityId, User currentUser) { Activity activity = activityRepository .findById(activityId) .orElseThrow(() -> new ActivityNotFoundException("Activity with ID: " + activityId + " not found")); + securityService.checkIfUserIsInGroup(currentUser, activity.getGroup()); + activity.setName(request.getName()); activity.setDescription(request.getDescription()); activity.setLink(request.getLink()); @@ -56,7 +54,12 @@ public ActivityResponse updateActivity(ActivityRequest request, Long activityId) return ActivityMapper.toResponse(activityRepository.save(activity)); } - public void deleteActivity(Long activityId) { + public void deleteActivity(Long activityId, User currentUser) { + Activity activity = activityRepository.findById(activityId) + .orElseThrow(() -> new ActivityNotFoundException("Activity with ID: " + activityId + " not found")); + + securityService.checkIfUserIsInGroup(currentUser, activity.getGroup()); + activityRepository.deleteById(activityId); } diff --git a/backend/src/main/java/share/fare/backend/service/GroupMembershipService.java b/backend/src/main/java/share/fare/backend/service/GroupMembershipService.java index b74e0e6..f597656 100644 --- a/backend/src/main/java/share/fare/backend/service/GroupMembershipService.java +++ b/backend/src/main/java/share/fare/backend/service/GroupMembershipService.java @@ -25,11 +25,14 @@ public class GroupMembershipService { private final GroupMembershipRepository groupMembershipRepository; private final GroupRepository groupRepository; private final UserRepository userRepository; + private final SecurityService securityService; - public GroupMembershipResponse addMemberToGroup(Long groupId, Long userId, GroupRole role, Long currentUserId) { + public GroupMembershipResponse addMemberToGroup(Long groupId, Long userId, GroupRole role, User currentUser) { Group group = groupRepository.findById(groupId) .orElseThrow(() -> new GroupNotFoundException(groupId)); + securityService.checkIfUserIsInGroup(currentUser, group); + User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException(userId)); @@ -47,7 +50,7 @@ public GroupMembershipResponse addMemberToGroup(Long groupId, Long userId, Group return GroupMembershipMapper.toResponse(savedMembership); } - public void removeMemberFromGroup(Long groupId, Long userId) { + public void removeMemberFromGroup(Long groupId, Long userId, User currentUser) { Group group = groupRepository.findById(groupId) .orElseThrow(() -> new GroupNotFoundException(groupId)); @@ -57,6 +60,8 @@ public void removeMemberFromGroup(Long groupId, Long userId) { GroupMembership membership = groupMembershipRepository.findByGroupAndUser(group, user) .orElseThrow(() -> new UserIsNotInGroupException("User is not a member of the group")); + securityService.checkIfUserCanRemoveMember(currentUser, user, group); + if (membership.getRole() == GroupRole.OWNER) { throw new IllegalArgumentException("Owner cannot be removed from the group"); } @@ -64,10 +69,12 @@ public void removeMemberFromGroup(Long groupId, Long userId) { groupMembershipRepository.delete(membership); } - public GroupMembershipResponse updateMemberRole(Long groupId, Long userId, GroupRole newRole) { + public GroupMembershipResponse updateMemberRole(Long groupId, Long userId, GroupRole newRole, User currentUser) { Group group = groupRepository.findById(groupId) .orElseThrow(() -> new GroupNotFoundException(groupId)); + securityService.checkIfUserIsGroupOwner(currentUser, group); + User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException(userId)); diff --git a/backend/src/main/java/share/fare/backend/service/GroupService.java b/backend/src/main/java/share/fare/backend/service/GroupService.java index 2fb6c71..66d3a08 100644 --- a/backend/src/main/java/share/fare/backend/service/GroupService.java +++ b/backend/src/main/java/share/fare/backend/service/GroupService.java @@ -22,6 +22,7 @@ public class GroupService { private final GroupRepository groupRepository; private final UserRepository userRepository; + private final SecurityService securityService; public GroupResponse createGroup(GroupRequest groupRequest, Long createdByUserId) { validateTripDates(groupRequest.getTripStartDate(), groupRequest.getTripEndDate()); @@ -50,19 +51,21 @@ public Page getGroupsForUser(Long userId, Pageable pageable) { return groups.map(GroupMapper::toResponse); } - public void deleteGroup(Long groupId) { - if (!groupRepository.existsById(groupId)) { - throw new GroupNotFoundException(groupId); - } + public void deleteGroup(Long groupId, User currentUser) { + Group group = groupRepository.findById(groupId) + .orElseThrow(() -> new GroupNotFoundException(groupId)); + securityService.checkIfUserIsGroupOwner(currentUser, group); groupRepository.deleteById(groupId); } - public GroupResponse updateGroup(Long groupId, GroupRequest groupRequest) { + public GroupResponse updateGroup(Long groupId, GroupRequest groupRequest, User currentUser) { validateTripDates(groupRequest.getTripStartDate(), groupRequest.getTripEndDate()); Group existingGroup = groupRepository.findById(groupId) .orElseThrow(() -> new GroupNotFoundException(groupId)); + securityService.checkIfUserIsGroupOwner(currentUser, existingGroup); + updateGroupFields(existingGroup, groupRequest); Group updatedGroup = groupRepository.save(existingGroup); diff --git a/backend/src/main/java/share/fare/backend/service/SecurityService.java b/backend/src/main/java/share/fare/backend/service/SecurityService.java new file mode 100644 index 0000000..e2cd3ec --- /dev/null +++ b/backend/src/main/java/share/fare/backend/service/SecurityService.java @@ -0,0 +1,42 @@ +package share.fare.backend.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import share.fare.backend.entity.Group; +import share.fare.backend.entity.GroupMembership; +import share.fare.backend.entity.GroupRole; +import share.fare.backend.entity.User; +import share.fare.backend.exception.UserIsNotGroupOwner; +import share.fare.backend.exception.UserIsNotInGroupException; +import share.fare.backend.repository.GroupMembershipRepository; + +@Service +@RequiredArgsConstructor +public class SecurityService { + private final GroupMembershipRepository membershipRepository; + + public void checkIfUserIsInGroup(User user, Group group) { + findMembershipOrThrow(user, group); + } + + public void checkIfUserIsGroupOwner(User user, Group group) { + GroupMembership membership = findMembershipOrThrow(user, group); + + if (membership.getRole() != GroupRole.OWNER) { + throw new UserIsNotGroupOwner(user.getId(), group.getId()); + } + } + + public void checkIfUserCanRemoveMember(User currentUser, User userToRemove, Group group) { + GroupMembership membership = findMembershipOrThrow(currentUser, group); + if (membership.getRole() != GroupRole.OWNER && !currentUser.getId().equals(userToRemove.getId())) { + throw new UserIsNotGroupOwner(currentUser.getId(), group.getId()); + } + } + + private GroupMembership findMembershipOrThrow(User user, Group group) { + return membershipRepository.findByGroupAndUser(group, user) + .orElseThrow(() -> + new UserIsNotInGroupException("User " + user.getId() + " is not in group " + group.getName())); + } +} diff --git a/backend/src/test/java/share/fare/backend/controller/GroupChatControllerTest.java b/backend/src/test/java/share/fare/backend/controller/GroupChatControllerTest.java new file mode 100644 index 0000000..ce184a2 --- /dev/null +++ b/backend/src/test/java/share/fare/backend/controller/GroupChatControllerTest.java @@ -0,0 +1,121 @@ +package share.fare.backend.controller; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import share.fare.backend.entity.*; +import share.fare.backend.repository.ChatMessageRepository; +import share.fare.backend.repository.GroupMembershipRepository; +import share.fare.backend.repository.GroupRepository; +import share.fare.backend.repository.UserRepository; + +import java.time.LocalDateTime; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class GroupChatControllerTest { + private static final String URI = "/api/v1/group/1/chat"; + + @Autowired + private UserRepository userRepository; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private GroupMembershipRepository groupMembershipRepository; + + @Autowired + private ChatMessageRepository chatMessageRepository; + + @Autowired + private MockMvc mockMvc; + + private User testUser; + private User testSecondUser; + private Group testGroup; + + @BeforeEach + void setUp() { + User user = User.builder() + .role(Role.USER) + .email("test@test.com") + .password("password") + .createdAt(LocalDateTime.now()) + .build(); + testUser = userRepository.save(user); + + Group group = Group.builder() + .name("group") + .description("group") + .createdBy(testUser) + .build(); + testGroup = groupRepository.save(group); + + User secondUser = User.builder() + .role(Role.USER) + .email("user2@test.com") + .password("password") + .createdAt(LocalDateTime.now()) + .build(); + testSecondUser = userRepository.save(secondUser); + + GroupMembership groupMembership = GroupMembership.builder() + .group(testGroup) + .user(testUser) + .build(); + groupMembershipRepository.save(groupMembership); + + ChatMessage message = ChatMessage.builder() + .groupId(testGroup.getId()) + .timestamp(LocalDateTime.now()) + .content("Hello") + .senderEmail(testUser.getEmail()) + .senderId(testUser.getId()) + .build(); + chatMessageRepository.save(message); + } + + @Test + public void getRecentMessagesSuccessTest() throws Exception { + mockMvc.perform(get("/api/v1/group/{groupId}/chat", testGroup.getId()) + .param("page", "0") + .param("size", "20") + .with(user(testUser)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content.[0].content").value("Hello")) + .andExpect(jsonPath("$.content[0].senderId").value(testUser.getId())); + } + + @Test + public void getRecentMessagesGroupDoesNotExisitTest() throws Exception { + mockMvc.perform(get("/api/v1/group/{groupId}/chat", testGroup.getId() + 5) + .param("page", "0") + .param("size", "20") + .with(user(testUser)) + ) + .andExpect(status().isNotFound()); + } + + @Test + public void getRecentMessagesUserNotInGroupTest() throws Exception { + mockMvc.perform(get("/api/v1/group/{groupId}/chat", testGroup.getId()) + .param("page", "0") + .param("size", "20") + .with(user(testSecondUser)) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("User " + testSecondUser.getId() + " is not in group " + testGroup.getName())); + } +} diff --git a/backend/src/test/java/share/fare/backend/controller/GroupControllerTest.java b/backend/src/test/java/share/fare/backend/controller/GroupControllerTest.java index 53f7562..2f0ec57 100644 --- a/backend/src/test/java/share/fare/backend/controller/GroupControllerTest.java +++ b/backend/src/test/java/share/fare/backend/controller/GroupControllerTest.java @@ -11,6 +11,8 @@ import org.springframework.test.web.servlet.MockMvc; import share.fare.backend.dto.request.GroupRequest; import share.fare.backend.entity.*; +import share.fare.backend.mapper.GroupMapper; +import share.fare.backend.repository.GroupMembershipRepository; import share.fare.backend.repository.GroupRepository; import share.fare.backend.repository.UserRepository; @@ -41,6 +43,9 @@ public class GroupControllerTest { @Autowired private GroupRepository groupRepository; + @Autowired + private GroupMembershipRepository groupMembershipRepository; + private User testUser; private Group testGroup; @@ -61,8 +66,15 @@ void setUp() { .createdAt(LocalDateTime.now()) .createdBy(testUser) .build(); - testGroup = groupRepository.save(group); + + GroupMembership groupMembership = GroupMembership.builder() + .group(testGroup) + .user(testUser) + .role(GroupRole.OWNER) + .build(); + + groupMembershipRepository.save(groupMembership); } @AfterEach diff --git a/backend/src/test/java/share/fare/backend/controller/GroupMembershipControllerTest.java b/backend/src/test/java/share/fare/backend/controller/GroupMembershipControllerTest.java index 71543d2..c869bc7 100644 --- a/backend/src/test/java/share/fare/backend/controller/GroupMembershipControllerTest.java +++ b/backend/src/test/java/share/fare/backend/controller/GroupMembershipControllerTest.java @@ -77,7 +77,13 @@ void setUp() { testGroup = groupRepository.save(group); - groupMembershipService.addMemberToGroup(testGroup.getId(), testUser.getId(), GroupRole.OWNER, testUser.getId()); + GroupMembership groupMembership = GroupMembership.builder() + .group(testGroup) + .user(testUser) + .role(GroupRole.OWNER) + .build(); + + groupMembershipRepository.save(groupMembership); } @AfterEach @@ -101,7 +107,7 @@ void addMemberTest() throws Exception { @Test void addMemberAlreadyIsInGroupTest() throws Exception { - groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser.getId()); + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser); mockMvc.perform(post(URI, testGroup.getId()) .param("userId", String.valueOf(newUser.getId())) .with(user(testUser)) @@ -131,7 +137,7 @@ void addMemberWithoutAuthTest() throws Exception { @Test void removeMemberTest() throws Exception { - groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser.getId()); + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser); mockMvc.perform(delete(URI + "/{userId}", testGroup.getId(), newUser.getId()) .with(user(testUser)) @@ -141,7 +147,7 @@ void removeMemberTest() throws Exception { @Test void removeOwnerTest() throws Exception { - groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser.getId()); + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.OWNER, testUser); mockMvc.perform(delete(URI + "/{userId}", testGroup.getId(), testUser.getId()) .with(user(newUser)) @@ -150,13 +156,22 @@ void removeOwnerTest() throws Exception { .andExpect(jsonPath("$.message").value("Owner cannot be removed from the group")); } + @Test + void userRemovesSelfFromGroupTest() throws Exception { + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser); + + mockMvc.perform(delete(URI + "/{userId}", testGroup.getId(), newUser.getId()) + .with(user(newUser)) + .principal(() -> String.valueOf(newUser.getId()))) + .andExpect(status().isNoContent()); + } + @Test void removeNonMemberTest() throws Exception { mockMvc.perform(delete(URI + "/{userId}", testGroup.getId(), newUser.getId()) .with(user(testUser)) .principal(() -> String.valueOf(testUser.getId()))) - .andExpect(status().isConflict()) - .andExpect(jsonPath("$.message").value("User is not a member of the group")); + .andExpect(status().isConflict()); } @Test @@ -171,7 +186,7 @@ void removeNonExistingMemberTest() throws Exception { @Test void updateMemberRoleTest() throws Exception { - groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser.getId()); + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser); mockMvc.perform(put(URI + "/{userId}/role", testGroup.getId(), newUser.getId()) .param("role", "ADMIN") @@ -183,7 +198,7 @@ void updateMemberRoleTest() throws Exception { @Test void getGroupMembersTest() throws Exception { - groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser.getId()); + groupMembershipService.addMemberToGroup(testGroup.getId(), newUser.getId(), GroupRole.MEMBER, testUser); mockMvc.perform(get(URI, testGroup.getId()) .with(user(testUser)) diff --git a/backend/src/test/java/share/fare/backend/service/ActivityServiceTest.java b/backend/src/test/java/share/fare/backend/service/ActivityServiceTest.java index ef0bf8c..0d1b83d 100644 --- a/backend/src/test/java/share/fare/backend/service/ActivityServiceTest.java +++ b/backend/src/test/java/share/fare/backend/service/ActivityServiceTest.java @@ -19,6 +19,7 @@ import share.fare.backend.entity.User; import share.fare.backend.exception.ActivityNotFoundException; import share.fare.backend.exception.GroupNotFoundException; +import share.fare.backend.exception.UserIsNotInGroupException; import share.fare.backend.exception.UserNotFoundException; import share.fare.backend.repository.ActivityRepository; import share.fare.backend.repository.GroupRepository; @@ -43,6 +44,9 @@ class ActivityServiceTest { @Mock private UserRepository userRepository; + @Mock + private SecurityService securityService; + @InjectMocks private ActivityService activityService; @@ -83,15 +87,13 @@ public void setUp() { @Test public void testCreateActivitySuccess() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); when(activityRepository.save(any(Activity.class))).thenReturn(testActivity); - ActivityResponse result = activityService.createActivity(testActivityRequest, 1L, 1L); + ActivityResponse result = activityService.createActivity(testActivityRequest, testUser, 1L); assertNotNull(result); assertEquals("Test Activity", result.getName()); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); verify(activityRepository, times(1)).save(any(Activity.class)); } @@ -99,26 +101,16 @@ public void testCreateActivitySuccess() { public void testCreateActivityGroupNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> activityService.createActivity(testActivityRequest, 1L, 1L)); + assertThrows(GroupNotFoundException.class, () -> activityService.createActivity(testActivityRequest, testUser, 1L)); verify(groupRepository, times(1)).findById(1L); } - @Test - public void testCreateActivityUserNotFound() { - when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.empty()); - - assertThrows(UserNotFoundException.class, () -> activityService.createActivity(testActivityRequest, 1L, 1L)); - verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - } - @Test public void testUpdateActivitySuccess() { when(activityRepository.findById(1L)).thenReturn(Optional.of(testActivity)); when(activityRepository.save(any(Activity.class))).thenReturn(testActivity); - ActivityResponse result = activityService.updateActivity(testActivityRequest, 1L); + ActivityResponse result = activityService.updateActivity(testActivityRequest, 1L, testUser); assertNotNull(result); assertEquals("Test Activity", result.getName()); @@ -130,19 +122,35 @@ public void testUpdateActivitySuccess() { public void testUpdateActivityActivityNotFound() { when(activityRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(ActivityNotFoundException.class, () -> activityService.updateActivity(testActivityRequest, 1L)); + assertThrows(ActivityNotFoundException.class, () -> activityService.updateActivity(testActivityRequest, 1L, testUser)); verify(activityRepository, times(1)).findById(1L); } @Test public void testDeleteActivitySuccess() { + when(activityRepository.findById(1L)).thenReturn(Optional.of(testActivity)); doNothing().when(activityRepository).deleteById(1L); - activityService.deleteActivity(1L); + activityService.deleteActivity(1L, testUser); verify(activityRepository, times(1)).deleteById(1L); } + @Test + public void testDeleteActivityUserNotGroupMember() { + when(activityRepository.findById(1L)).thenReturn(Optional.of(testActivity)); + + doThrow(new UserIsNotInGroupException("User with ID " + testUser.getId() + " is not in group")) + .when(securityService).checkIfUserIsInGroup(testUser, testGroup); + + assertThrows(UserIsNotInGroupException.class, () -> { + activityService.deleteActivity(1L, testUser); + }); + + verify(activityRepository, never()).deleteById(anyLong()); + } + + @Test public void testGetActivitiesForGroupSuccess() { Pageable pageable = PageRequest.of(0, 10); @@ -156,5 +164,4 @@ public void testGetActivitiesForGroupSuccess() { assertEquals("Test Activity", result.getContent().getFirst().getName()); verify(activityRepository, times(1)).findByGroup_Id(1L, pageable); } - } \ No newline at end of file diff --git a/backend/src/test/java/share/fare/backend/service/GroupMembershipServiceTest.java b/backend/src/test/java/share/fare/backend/service/GroupMembershipServiceTest.java index de394e4..d039701 100644 --- a/backend/src/test/java/share/fare/backend/service/GroupMembershipServiceTest.java +++ b/backend/src/test/java/share/fare/backend/service/GroupMembershipServiceTest.java @@ -39,10 +39,14 @@ class GroupMembershipServiceTest { @Mock private UserRepository userRepository; + @Mock + private SecurityService securityService; + @InjectMocks private GroupMembershipService groupMembershipService; private User testUser; + private User testUser2; private Group testGroup; private GroupMembership testMembership; @@ -54,6 +58,12 @@ public void setUp() { .password("password") .build(); + testUser2 = User.builder() + .id(2L) + .email("test2@test.com") + .password("password2") + .build(); + testGroup = Group.builder() .id(1L) .name("Test Group") @@ -75,7 +85,7 @@ public void testAddMemberToGroupSuccess() { when(groupMembershipRepository.existsByGroupAndUser(testGroup, testUser)).thenReturn(false); when(groupMembershipRepository.save(any(GroupMembership.class))).thenReturn(testMembership); - GroupMembershipResponse result = groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, 1L); + GroupMembershipResponse result = groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, testUser); assertNotNull(result); assertEquals(1L, result.getUserId()); @@ -89,7 +99,7 @@ public void testAddMemberToGroupSuccess() { public void testAddMemberToGroupGroupNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, 1L)); + assertThrows(GroupNotFoundException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, testUser)); verify(groupRepository, times(1)).findById(1L); } @@ -98,7 +108,7 @@ public void testAddMemberToGroupUserNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); when(userRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(UserNotFoundException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, 1L)); + assertThrows(UserNotFoundException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, testUser)); verify(groupRepository, times(1)).findById(1L); verify(userRepository, times(1)).findById(1L); } @@ -109,7 +119,7 @@ public void testAddMemberToGroupUserAlreadyInGroup() { when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); when(groupMembershipRepository.existsByGroupAndUser(testGroup, testUser)).thenReturn(true); - assertThrows(UserAlreadyInGroupException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, 1L)); + assertThrows(UserAlreadyInGroupException.class, () -> groupMembershipService.addMemberToGroup(1L, 1L, GroupRole.MEMBER, testUser)); verify(groupRepository, times(1)).findById(1L); verify(userRepository, times(1)).findById(1L); verify(groupMembershipRepository, times(1)).existsByGroupAndUser(testGroup, testUser); @@ -118,15 +128,14 @@ public void testAddMemberToGroupUserAlreadyInGroup() { @Test public void testRemoveMemberFromGroupSuccess() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); - when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser)).thenReturn(Optional.of(testMembership)); + when(userRepository.findById(2L)).thenReturn(Optional.ofNullable(testUser2)); + when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser2)).thenReturn(Optional.ofNullable(testMembership)); doNothing().when(groupMembershipRepository).delete(testMembership); - groupMembershipService.removeMemberFromGroup(1L, 1L); + groupMembershipService.removeMemberFromGroup(1L, 2L, testUser); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser); + verify(userRepository, times(1)).findById(2L); verify(groupMembershipRepository, times(1)).delete(testMembership); } @@ -134,59 +143,59 @@ public void testRemoveMemberFromGroupSuccess() { public void testRemoveMemberFromGroupGroupNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 1L)); + assertThrows(GroupNotFoundException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 2L, testUser)); verify(groupRepository, times(1)).findById(1L); } @Test public void testRemoveMemberFromGroupUserNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.empty()); + when(userRepository.findById(2L)).thenReturn(Optional.empty()); - assertThrows(UserNotFoundException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 1L)); + assertThrows(UserNotFoundException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 2L, testUser)); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); + verify(userRepository, times(1)).findById(2L); } @Test public void testRemoveMemberFromGroupUserNotInGroup() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); - when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser)).thenReturn(Optional.empty()); + when(userRepository.findById(2L)).thenReturn(Optional.of(testUser2)); + when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser2)).thenReturn(Optional.empty()); - assertThrows(UserIsNotInGroupException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 1L)); + assertThrows(UserIsNotInGroupException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 2L, testUser)); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser); + verify(userRepository, times(1)).findById(2L); + verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser2); } @Test public void testRemoveMemberFromGroupOwnerCannotBeRemoved() { testMembership.setRole(GroupRole.OWNER); when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); - when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser)).thenReturn(Optional.of(testMembership)); + when(userRepository.findById(2L)).thenReturn(Optional.of(testUser2)); + when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser2)).thenReturn(Optional.of(testMembership)); - assertThrows(IllegalArgumentException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 1L)); + assertThrows(IllegalArgumentException.class, () -> groupMembershipService.removeMemberFromGroup(1L, 2L, testUser)); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser); + verify(userRepository, times(1)).findById(2L); + verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser2); } @Test public void testUpdateMemberRoleSuccess() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); - when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser)).thenReturn(Optional.of(testMembership)); + when(userRepository.findById(2L)).thenReturn(Optional.of(testUser2)); + when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser2)).thenReturn(Optional.of(testMembership)); when(groupMembershipRepository.save(any(GroupMembership.class))).thenReturn(testMembership); - GroupMembershipResponse result = groupMembershipService.updateMemberRole(1L, 1L, GroupRole.ADMIN); + GroupMembershipResponse result = groupMembershipService.updateMemberRole(1L, 2L, GroupRole.ADMIN, testUser); assertNotNull(result); assertEquals(1L, result.getUserId()); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser); + verify(userRepository, times(1)).findById(2L); + verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser2); verify(groupMembershipRepository, times(1)).save(any(GroupMembership.class)); } @@ -194,30 +203,30 @@ public void testUpdateMemberRoleSuccess() { public void testUpdateMemberRoleGroupNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> groupMembershipService.updateMemberRole(1L, 1L, GroupRole.ADMIN)); + assertThrows(GroupNotFoundException.class, () -> groupMembershipService.updateMemberRole(1L, 2L, GroupRole.ADMIN, testUser)); verify(groupRepository, times(1)).findById(1L); } @Test public void testUpdateMemberRoleUserNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.empty()); + when(userRepository.findById(2L)).thenReturn(Optional.empty()); - assertThrows(UserNotFoundException.class, () -> groupMembershipService.updateMemberRole(1L, 1L, GroupRole.ADMIN)); + assertThrows(UserNotFoundException.class, () -> groupMembershipService.updateMemberRole(1L, 2L, GroupRole.ADMIN, testUser)); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); + verify(userRepository, times(1)).findById(2L); } @Test public void testUpdateMemberRoleUserNotInGroup() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); - when(userRepository.findById(1L)).thenReturn(Optional.of(testUser)); - when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser)).thenReturn(Optional.empty()); + when(userRepository.findById(2L)).thenReturn(Optional.of(testUser2)); + when(groupMembershipRepository.findByGroupAndUser(testGroup, testUser2)).thenReturn(Optional.empty()); - assertThrows(UserIsNotInGroupException.class, () -> groupMembershipService.updateMemberRole(1L, 1L, GroupRole.ADMIN)); + assertThrows(UserIsNotInGroupException.class, () -> groupMembershipService.updateMemberRole(1L, 2L, GroupRole.ADMIN, testUser)); verify(groupRepository, times(1)).findById(1L); - verify(userRepository, times(1)).findById(1L); - verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser); + verify(userRepository, times(1)).findById(2L); + verify(groupMembershipRepository, times(1)).findByGroupAndUser(testGroup, testUser2); } @Test diff --git a/backend/src/test/java/share/fare/backend/service/GroupServiceTest.java b/backend/src/test/java/share/fare/backend/service/GroupServiceTest.java index 98e93b9..8fb4a00 100644 --- a/backend/src/test/java/share/fare/backend/service/GroupServiceTest.java +++ b/backend/src/test/java/share/fare/backend/service/GroupServiceTest.java @@ -36,6 +36,9 @@ class GroupServiceTest { @Mock private UserRepository userRepository; + @Mock + private SecurityService securityService; + @InjectMocks private GroupService groupService; @@ -149,21 +152,21 @@ public void testGetGroupsForUser() { @Test public void testDeleteGroupSuccess() { - when(groupRepository.existsById(1L)).thenReturn(true); + when(groupRepository.findById(1L)).thenReturn(Optional.ofNullable(testGroup)); doNothing().when(groupRepository).deleteById(1L); - groupService.deleteGroup(1L); + groupService.deleteGroup(1L, testUser); - verify(groupRepository, times(1)).existsById(1L); + verify(groupRepository, times(1)).findById(1L); verify(groupRepository, times(1)).deleteById(1L); } @Test public void testDeleteGroupNotFound() { - when(groupRepository.existsById(1L)).thenReturn(false); + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> groupService.deleteGroup(1L)); - verify(groupRepository, times(1)).existsById(1L); + assertThrows(GroupNotFoundException.class, () -> groupService.deleteGroup(1L, testUser)); + verify(groupRepository, times(1)).findById(1L); } @Test @@ -171,7 +174,7 @@ public void testUpdateGroupSuccess() { when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); when(groupRepository.save(any(Group.class))).thenReturn(testGroup); - GroupResponse result = groupService.updateGroup(1L, testGroupRequest); + GroupResponse result = groupService.updateGroup(1L, testGroupRequest, testUser); assertNotNull(result); assertEquals("Test Group", result.getName()); @@ -183,7 +186,7 @@ public void testUpdateGroupSuccess() { public void testUpdateGroupNotFound() { when(groupRepository.findById(1L)).thenReturn(Optional.empty()); - assertThrows(GroupNotFoundException.class, () -> groupService.updateGroup(1L, testGroupRequest)); + assertThrows(GroupNotFoundException.class, () -> groupService.updateGroup(1L, testGroupRequest, testUser)); verify(groupRepository, times(1)).findById(1L); } @@ -196,6 +199,6 @@ public void testUpdateGroupInvalidTripDates() { .tripEndDate(LocalDate.now().plusDays(1)) .build(); - assertThrows(IllegalArgumentException.class, () -> groupService.updateGroup(1L, invalidRequest)); + assertThrows(IllegalArgumentException.class, () -> groupService.updateGroup(1L, invalidRequest, testUser)); } } \ No newline at end of file From 213a428b9b42932b6da6a46bc97b45c1af2b6d14 Mon Sep 17 00:00:00 2001 From: szymongrabski Date: Wed, 11 Jun 2025 12:49:24 +0200 Subject: [PATCH 2/3] add group balance service test --- .../service/GroupBalanceServiceTest.java | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 backend/src/test/java/share/fare/backend/service/GroupBalanceServiceTest.java diff --git a/backend/src/test/java/share/fare/backend/service/GroupBalanceServiceTest.java b/backend/src/test/java/share/fare/backend/service/GroupBalanceServiceTest.java new file mode 100644 index 0000000..14b4e0a --- /dev/null +++ b/backend/src/test/java/share/fare/backend/service/GroupBalanceServiceTest.java @@ -0,0 +1,171 @@ +package share.fare.backend.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import share.fare.backend.dto.response.GroupBalanceResponse; +import share.fare.backend.dto.response.TransactionResponse; +import share.fare.backend.entity.*; +import share.fare.backend.exception.GroupNotFoundException; +import share.fare.backend.repository.GroupBalanceRepository; +import share.fare.backend.repository.GroupRepository; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class GroupBalanceServiceTest { + + @Mock + private GroupBalanceRepository groupBalanceRepository; + + @Mock + private GroupRepository groupRepository; + + @InjectMocks + private GroupBalanceService groupBalanceService; + + private User testUser1; + private User testUser2; + private Group testGroup; + private Expense testExpense; + private ExpenseAllocation testAllocation1; + private ExpenseAllocation testAllocation2; + + @BeforeEach + public void setUp() { + testUser1 = User.builder() + .id(1L) + .email("user1@test.com") + .password("password") + .build(); + + testUser2 = User.builder() + .id(2L) + .email("user2@test.com") + .password("password") + .build(); + + testGroup = Group.builder() + .id(1L) + .name("Test Group") + .description("Test Description") + .build(); + + testExpense = Expense.builder() + .id(1L) + .description("Test Expense") + .totalAmount(new BigDecimal("100.00")) + .paidByUser(testUser1) + .group(testGroup) + .build(); + + testAllocation1 = ExpenseAllocation.builder() + .user(testUser1) + .amountOwed(new BigDecimal("50.00")) + .expense(testExpense) + .build(); + + testAllocation2 = ExpenseAllocation.builder() + .user(testUser2) + .amountOwed(new BigDecimal("50.00")) + .expense(testExpense) + .build(); + + testExpense.setExpenseAllocations(List.of(testAllocation1, testAllocation2)); + } + + @Test + public void testUpdateBalances_ExistingBalances() { + GroupBalance existingPayerBalance = new GroupBalance(testGroup, testUser1, new BigDecimal("200.00")); + GroupBalance existingDebtorBalance = new GroupBalance(testGroup, testUser2, new BigDecimal("-100.00")); + + when(groupBalanceRepository.findByGroupAndUser(testGroup, testUser1)) + .thenReturn(Optional.of(existingPayerBalance)); + when(groupBalanceRepository.findByGroupAndUser(testGroup, testUser2)) + .thenReturn(Optional.of(existingDebtorBalance)); + + groupBalanceService.updateBalances(testExpense); + + assertEquals(new BigDecimal("250.00"), existingPayerBalance.getBalance()); + assertEquals(new BigDecimal("-150.00"), existingDebtorBalance.getBalance()); + verify(groupBalanceRepository, times(3)).save(any(GroupBalance.class)); + } + + @Test + public void testGetBalances_Success() { + GroupBalance balance1 = new GroupBalance(testGroup, testUser1, new BigDecimal("100.00")); + GroupBalance balance2 = new GroupBalance(testGroup, testUser2, new BigDecimal("-50.00")); + + when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); + when(groupBalanceRepository.findByGroup(testGroup)).thenReturn(List.of(balance1, balance2)); + + List result = groupBalanceService.getBalances(1L); + + assertNotNull(result); + assertEquals(2, result.size()); + verify(groupRepository, times(1)).findById(1L); + verify(groupBalanceRepository, times(1)).findByGroup(testGroup); + } + + @Test + public void testGetBalances_GroupNotFound() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(GroupNotFoundException.class, () -> groupBalanceService.getBalances(1L)); + verify(groupRepository, times(1)).findById(1L); + } + + @Test + public void testGetMinimumTransactions_Success() { + GroupBalance debtor1 = new GroupBalance(testGroup, testUser1, new BigDecimal("-50.00")); + GroupBalance debtor2 = new GroupBalance(testGroup, testUser2, new BigDecimal("-30.00")); + User creditorUser = User.builder().id(3L).build(); + GroupBalance creditor = new GroupBalance(testGroup, creditorUser, new BigDecimal("80.00")); + + when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); + when(groupBalanceRepository.findByGroup(testGroup)).thenReturn(List.of(debtor1, debtor2, creditor)); + + List result = groupBalanceService.getMinimumTransactions(1L); + + assertNotNull(result); + assertEquals(2, result.size()); + + TransactionResponse firstTransaction = result.get(0); + assertEquals(new BigDecimal("30.00"), firstTransaction.getAmount()); + + TransactionResponse secondTransaction = result.get(1); + assertEquals(new BigDecimal("50.00"), secondTransaction.getAmount()); + + verify(groupRepository, times(1)).findById(1L); + verify(groupBalanceRepository, times(1)).findByGroup(testGroup); + } + + @Test + public void testGetMinimumTransactions_GroupNotFound() { + when(groupRepository.findById(1L)).thenReturn(Optional.empty()); + + assertThrows(GroupNotFoundException.class, () -> groupBalanceService.getMinimumTransactions(1L)); + verify(groupRepository, times(1)).findById(1L); + } + + @Test + public void testGetMinimumTransactions_NoBalances() { + when(groupRepository.findById(1L)).thenReturn(Optional.of(testGroup)); + when(groupBalanceRepository.findByGroup(testGroup)).thenReturn(Collections.emptyList()); + + List result = groupBalanceService.getMinimumTransactions(1L); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + +} \ No newline at end of file From 1743e02fb34b067d2f1a104fe7e705be9f8f718b Mon Sep 17 00:00:00 2001 From: Agata Bartczak Date: Mon, 23 Jun 2025 22:47:32 +0200 Subject: [PATCH 3/3] add redirect when error appears fetching a trip --- frontend/app/(root)/trips/[id]/chat/page.tsx | 5 ++++- frontend/context/GroupChatContext.tsx | 3 +++ frontend/context/TripContext.tsx | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/app/(root)/trips/[id]/chat/page.tsx b/frontend/app/(root)/trips/[id]/chat/page.tsx index bf0b0f1..0a51e00 100644 --- a/frontend/app/(root)/trips/[id]/chat/page.tsx +++ b/frontend/app/(root)/trips/[id]/chat/page.tsx @@ -17,6 +17,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { useState, useRef, useEffect, useCallback } from 'react'; +import Link from 'next/link'; export default function ChatPage() { const router = useRouter(); @@ -126,7 +127,9 @@ export default function ChatPage() { > {!isOwnMessage && (
- {msg.senderEmail} + + {msg.senderEmail} +
)} diff --git a/frontend/context/GroupChatContext.tsx b/frontend/context/GroupChatContext.tsx index 8b5078a..14f9618 100644 --- a/frontend/context/GroupChatContext.tsx +++ b/frontend/context/GroupChatContext.tsx @@ -13,6 +13,7 @@ import SockJS from 'sockjs-client'; import { getToken } from '@/lib/auth'; import axiosInstance from '@/lib/axiosInstance'; import { ChatMessage } from '@/validation/chatMessageSchema'; +import { useRouter } from 'next/navigation'; interface GroupChatContextType { messages: ChatMessage[]; @@ -40,6 +41,7 @@ export const GroupChatProvider = ({ const [isLoading, setIsLoading] = useState(false); const pageSize = 10; + const router = useRouter(); const fetchMessages = useCallback( async (pageNumber: number) => { try { @@ -62,6 +64,7 @@ export const GroupChatProvider = ({ setHasMore(!res.data.last); } catch (err) { console.error('Error fetching chat messages:', err); + router.push('/trips'); } finally { setIsLoading(false); } diff --git a/frontend/context/TripContext.tsx b/frontend/context/TripContext.tsx index 159102d..c861e47 100644 --- a/frontend/context/TripContext.tsx +++ b/frontend/context/TripContext.tsx @@ -11,6 +11,7 @@ import axiosInstance from '@/lib/axiosInstance'; import { useParams } from 'next/navigation'; import { getToken, decodeToken } from '@/lib/auth'; import { Group, groupSchema } from '@/validation/groupSchema'; +import { useRouter } from 'next/navigation'; interface TripContextType { trip: Group | null; @@ -32,6 +33,7 @@ export function TripContextProvider({ children }: { children: ReactNode }) { const [trip, setTrip] = useState(null); const [loading, setLoading] = useState(true); const [tripError, setTripError] = useState(null); + const router = useRouter(); useEffect(() => { if (!params.id) return; @@ -62,6 +64,7 @@ export function TripContextProvider({ children }: { children: ReactNode }) { } catch (err) { console.error(err); setTripError('Failed to fetch trip'); + router.push('/trips'); } finally { setLoading(false); }