diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 0775f6f..d1d0157 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -264,6 +264,33 @@ All API requests should be made to the following base URL (Spring Boot's default - **Add a Member to a Team:** `POST /{teamMemberId}/team/{teamId}` - **Description:** Adds a team member to a team. +- **Mass Assign to a Team:** `POST /team/{teamId}/mass-assign` + - **Request Body:** + ```json + [1, 2, 3, 4] + ``` + - **Response Body:** + ```json + [ + { + "isMemberOfId": 101, + "teamMemberId": 1, + "teamId": 42 + }, + { + "isMemberOfId": 102, + "teamMemberId": 2, + "teamId": 42 + }, + { + "isMemberOfId": 103, + "teamMemberId": 3, + "teamId": 42 + } + ] + ``` + - **Description:** Adds multiple members to a team at once. If any member is already in the team they will be skipped. If any member does not exist, the entire request will fail with a 400 error. + - **Remove a Member from a Team:** `DELETE /{teamMemberId}/team/{teamId}` - **Description:** Removes a team member from a team. @@ -589,7 +616,7 @@ All API requests should be made to the following base URL (Spring Boot's default - **Mass Assign Members to a Task:** `POST /{taskId}/mass-assign` - **Request Body:** ```json - [1, 2, 3, 4] + [1, 2, 3, 4] ``` - **Response Body:** ```json diff --git a/backend/task-manager/src/main/java/com/example/task_manager/controller/IsMemberOfController.java b/backend/task-manager/src/main/java/com/example/task_manager/controller/IsMemberOfController.java index 252a0ea..8f2bd4a 100644 --- a/backend/task-manager/src/main/java/com/example/task_manager/controller/IsMemberOfController.java +++ b/backend/task-manager/src/main/java/com/example/task_manager/controller/IsMemberOfController.java @@ -1,6 +1,10 @@ package com.example.task_manager.controller; +import com.example.task_manager.DTO.IsMemberOfDTO; import com.example.task_manager.service.IsMemberOfService; + +import java.util.List; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,6 +29,18 @@ public ResponseEntity addMemberToTeam(@PathVariable int teamMemberId, @PathVa } } + //mass assign members to a team + @PostMapping("/team/{teamId}/mass-assign") + public ResponseEntity massAssignToTeam(@PathVariable int teamId, @RequestBody List teamMemberIds) { + try { + List isMemberOfDTOs = isMemberOfService.massAssignToTeam(teamId, teamMemberIds); + return ResponseEntity.ok(isMemberOfDTOs); + } + catch (RuntimeException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } + // Remove Member from Team @DeleteMapping("/{teamMemberId}/team/{teamId}") public ResponseEntity removeMemberFromTeam(@PathVariable int teamMemberId, @PathVariable int teamId) { diff --git a/backend/task-manager/src/main/java/com/example/task_manager/controller/TeamMemberController.java b/backend/task-manager/src/main/java/com/example/task_manager/controller/TeamMemberController.java index 5c798f6..b173d22 100644 --- a/backend/task-manager/src/main/java/com/example/task_manager/controller/TeamMemberController.java +++ b/backend/task-manager/src/main/java/com/example/task_manager/controller/TeamMemberController.java @@ -5,11 +5,14 @@ import com.example.task_manager.DTO.TaskDTO; import com.example.task_manager.DTO.TeamDTO; import com.example.task_manager.service.TeamMemberService; + import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import com.example.task_manager.service.IsMemberOfService; + @RestController @RequestMapping("/api/members/actions") public class TeamMemberController { @@ -37,8 +40,7 @@ public ResponseEntity massAssignToTask(@PathVariable int taskId, @RequestBody try { List isAssignedDTOs = teamMemberService.massAssignToTask(taskId, teamMemberIds); return ResponseEntity.ok(isAssignedDTOs); - } - catch (RuntimeException e) { + } catch (RuntimeException e) { return ResponseEntity.badRequest().body(e.getMessage()); } } diff --git a/backend/task-manager/src/main/java/com/example/task_manager/service/IsMemberOfService.java b/backend/task-manager/src/main/java/com/example/task_manager/service/IsMemberOfService.java index 5ab3c22..fe40b3c 100644 --- a/backend/task-manager/src/main/java/com/example/task_manager/service/IsMemberOfService.java +++ b/backend/task-manager/src/main/java/com/example/task_manager/service/IsMemberOfService.java @@ -1,5 +1,8 @@ package com.example.task_manager.service; +import java.util.ArrayList; +import java.util.List; + import org.springframework.stereotype.Service; import com.example.task_manager.DTO.IsMemberOfDTO; @@ -40,42 +43,69 @@ public IsMemberOfService(IsMemberOfRepository isMemberOfRepository, * @param teamId The ID of the team to which the member should be added. * @return */ - public IsMemberOfDTO addMemberToTeam(int teamMemberId, int teamId) { - System.out.println("BELLO it is running yay"); + public IsMemberOfDTO addMemberToTeam(int teamMemberId, int teamId) { + System.out.println("BELLO it is running yay"); - Team team = teamRepository.findById(teamId) - .orElseThrow(() -> new RuntimeException("Team not found with ID: " + teamId)); - - TeamMember teamMember = teamMemberRepository.findById(teamMemberId) - .orElseThrow(() -> new RuntimeException("Team Member not found with ID: " + teamMemberId)); + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new RuntimeException("Team not found with ID: " + teamId)); - System.out.println("MINIONS"); - - // Check if the member is already in the team - boolean alreadyMember = isMemberOfRepository.existsByTeamMemberAccountIdAndTeamTeamId(teamMemberId, teamId); - if (alreadyMember) { - throw new RuntimeException("Team Member is already in this team. No action needed."); - } + TeamMember teamMember = teamMemberRepository.findById(teamMemberId) + .orElseThrow(() -> new RuntimeException("Team Member not found with ID: " + teamMemberId)); - System.out.println("TONIGHT!!! WE! ARE GOING! TO STEAL! THE MOON!!"); - + System.out.println("MINIONS"); - // Create membership - IsMemberOf isMemberOf = new IsMemberOf(teamMember, team); - isMemberOf = isMemberOfRepository.save(isMemberOf); - isMemberOfRepository.flush(); + // Check if the member is already in the team + boolean alreadyMember = isMemberOfRepository.existsByTeamMemberAccountIdAndTeamTeamId(teamMemberId, teamId); + if (alreadyMember) { + throw new RuntimeException("Team Member is already in this team. No action needed."); + } - System.out.println("DOCTOR NEFARIO"); + System.out.println("TONIGHT!!! WE! ARE GOING! TO STEAL! THE MOON!!"); - team = teamRepository.findById(teamId).orElseThrow(() -> new RuntimeException("RAHHHH can't find it")); - System.out.println("bruh"); + // Create membership + IsMemberOf isMemberOf = new IsMemberOf(teamMember, team); + isMemberOf = isMemberOfRepository.save(isMemberOf); + isMemberOfRepository.flush(); - //call assigned to team notification method - notifService.notifyTeamAssignment(teamMember, team); + System.out.println("DOCTOR NEFARIO"); - // Return DTO - return convertToDTO(isMemberOf); - } + team = teamRepository.findById(teamId).orElseThrow(() -> new RuntimeException("RAHHHH can't find it")); + System.out.println("bruh"); + + //call assigned to team notification method + notifService.notifyTeamAssignment(teamMember, team); + + // Return DTO + return convertToDTO(isMemberOf); + } + + //add multiple members to a team at once + public List massAssignToTeam(int teamId, List teamMemberIds) { + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new RuntimeException("Team not found with ID: " + teamId)); + + List newMembers = new ArrayList<>(); + + for (Integer teamMemberId : teamMemberIds) { + TeamMember teamMember = teamMemberRepository.findById(teamMemberId) + .orElseThrow(() -> new RuntimeException("Team Member not found with ID: " + teamMemberId)); + + boolean alreadyMember = isMemberOfRepository.existsByTeamMemberAccountIdAndTeamTeamId(teamMemberId, teamId); + + if (!alreadyMember) { + IsMemberOf isMemberOf = new IsMemberOf(teamMember, team); + isMemberOf = isMemberOfRepository.save(isMemberOf); + + notifService.notifyTeamAssignment(teamMember, team); + + newMembers.add(convertToDTO(isMemberOf)); + } + } + + isMemberOfRepository.flush(); + + return newMembers; + } /** * Removes a TeamMember from a Team. @@ -122,8 +152,8 @@ public boolean isMemberOfTeam(int teamMemberId, int teamId) { private IsMemberOfDTO convertToDTO(IsMemberOf isMemberOf) { return new IsMemberOfDTO( isMemberOf.getId(), - isMemberOf.getTeam().getTeamId(), - isMemberOf.getTeamMember().getAccountId() + isMemberOf.getTeamMember().getAccountId(), + isMemberOf.getTeam().getTeamId() ); } } diff --git a/backend/task-manager/src/test/java/com/example/task_manager/TaskManagerApplicationTests.java b/backend/task-manager/src/test/java/com/example/task_manager/TaskManagerApplicationTests.java index 5855d02..a4a9ab4 100644 --- a/backend/task-manager/src/test/java/com/example/task_manager/TaskManagerApplicationTests.java +++ b/backend/task-manager/src/test/java/com/example/task_manager/TaskManagerApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class TaskManagerApplicationTests { @Test diff --git a/backend/task-manager/src/test/java/com/example/task_manager/controller_tests/IsMemberOfControllerTest.java b/backend/task-manager/src/test/java/com/example/task_manager/controller_tests/IsMemberOfControllerTest.java index c9ad0d2..43f27b7 100644 --- a/backend/task-manager/src/test/java/com/example/task_manager/controller_tests/IsMemberOfControllerTest.java +++ b/backend/task-manager/src/test/java/com/example/task_manager/controller_tests/IsMemberOfControllerTest.java @@ -1,7 +1,11 @@ package com.example.task_manager.controller_tests; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import com.example.task_manager.DTO.IsMemberOfDTO; @@ -44,6 +48,50 @@ void testAddMemberToTeam() throws Exception { .andExpect(jsonPath("$.teamId").value(teamId)); } + /* + * Test adding multiple members to a team at once + */ + @Test + void testMassAssignToTeam() throws Exception { + int teamId = 100; + List teamMemberIds = List.of(101, 102, 103); + + List mockMemberships = List.of( + new IsMemberOfDTO(1, 101, teamId), + new IsMemberOfDTO(2, 102, teamId), + new IsMemberOfDTO(3, 103, teamId)); + + when(isMemberOfService.massAssignToTeam(eq(teamId), eq(teamMemberIds))).thenReturn(mockMemberships); + + String jsonRequest = "[101, 102, 103]"; + + mockMvc.perform(post("/api/memberships/team/" + teamId + "/mass-assign") + .contentType("application/json") + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(3)) + .andExpect(jsonPath("$[0].teamMemberId").value(101)) + .andExpect(jsonPath("$[1].teamMemberId").value(102)) + .andExpect(jsonPath("$[2].teamMemberId").value(103)); + } + + @Test + void testMassAssignToTeam_TeamNotFound() throws Exception { + int teamId = 999; + List teamMemberIds = List.of(101, 102, 103); + + String jsonRequest = "[101, 102, 103]"; + + when(isMemberOfService.massAssignToTeam(eq(teamId), eq(teamMemberIds))) + .thenThrow(new RuntimeException("Team not found")); + + mockMvc.perform(post("/api/memberships/team/" + teamId + "/mass-assign") + .contentType("application/json") + .content(jsonRequest)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("Team not found")); + } + /** * Test Remove Member from Team */ diff --git a/backend/task-manager/src/test/java/com/example/task_manager/service_tests/IsMemberOfServiceTest.java b/backend/task-manager/src/test/java/com/example/task_manager/service_tests/IsMemberOfServiceTest.java index b27404a..a42548b 100644 --- a/backend/task-manager/src/test/java/com/example/task_manager/service_tests/IsMemberOfServiceTest.java +++ b/backend/task-manager/src/test/java/com/example/task_manager/service_tests/IsMemberOfServiceTest.java @@ -2,6 +2,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; @@ -9,6 +11,7 @@ import jakarta.transaction.Transactional; +import com.example.task_manager.DTO.IsMemberOfDTO; import com.example.task_manager.DTO.TeamDTO; import com.example.task_manager.DTO.TeamMemberDTO; import com.example.task_manager.test_helpers.ServiceTestHelper; @@ -60,10 +63,88 @@ void testAddMemberToSameTeamTwice() { isMemberOfService.addMemberToTeam(teamMember.getAccountId(), team.getTeamId()); Exception exception = assertThrows(RuntimeException.class, - () -> isMemberOfService.addMemberToTeam(teamMember.getAccountId(), team.getTeamId())); + () -> isMemberOfService.addMemberToTeam(teamMember.getAccountId(), team.getTeamId())); assertTrue(exception.getMessage().contains("Team Member is already in this team")); } + + @Test + void testMassAddMembersToTeam() { + TeamMemberDTO teamLead = createUniqueTeamMemberDTO(); + TeamDTO team = createUniqueTeamDTO(teamLead); + + TeamMemberDTO member1 = createUniqueTeamMemberDTO(); + TeamMemberDTO member2 = createUniqueTeamMemberDTO(); + TeamMemberDTO member3 = createUniqueTeamMemberDTO(); + + List memberIds = List.of( + member1.getAccountId(), + member2.getAccountId(), + member3.getAccountId() + ); + + List results = isMemberOfService.massAssignToTeam(team.getTeamId(), memberIds); + + assertEquals(3, results.size()); + + assertTrue(isMemberOfService.isMemberOfTeam(member1.getAccountId(), team.getTeamId())); + assertTrue(isMemberOfService.isMemberOfTeam(member2.getAccountId(), team.getTeamId())); + assertTrue(isMemberOfService.isMemberOfTeam(member3.getAccountId(), team.getTeamId())); + } + + @Test + void testMassAddMembersWithOneNonExistentId() { + TeamMemberDTO teamLead = createUniqueTeamMemberDTO(); + TeamDTO team = createUniqueTeamDTO(teamLead); + + TeamMemberDTO member1 = createUniqueTeamMemberDTO(); + TeamMemberDTO member2 = createUniqueTeamMemberDTO(); + int nonExistentId = 999999; + + List memberIds = List.of( + member1.getAccountId(), + nonExistentId, + member2.getAccountId() + ); + + Exception exception = assertThrows(RuntimeException.class, () -> + isMemberOfService.massAssignToTeam(team.getTeamId(), memberIds) + ); + + assertTrue(exception.getMessage().contains("Team Member not found")); + } + + @Test + void testMassAddMembersToNonExistentTeam() { + TeamMemberDTO member = createUniqueTeamMemberDTO(); + + Exception exception = assertThrows(RuntimeException.class, () -> + isMemberOfService.massAssignToTeam(999999, List.of(member.getAccountId())) + ); + + assertTrue(exception.getMessage().contains("Team not found")); + } + + @Test + void testMassAddMembersAlreadyInTeam() { + TeamMemberDTO teamLead = createUniqueTeamMemberDTO(); + TeamDTO team = createUniqueTeamDTO(teamLead); + + TeamMemberDTO member1 = createUniqueTeamMemberDTO(); + TeamMemberDTO member2 = createUniqueTeamMemberDTO(); + + isMemberOfService.addMemberToTeam(member1.getAccountId(), team.getTeamId()); + + List memberIds = List.of( + member1.getAccountId(), // already in team + member2.getAccountId() + ); + + List results = isMemberOfService.massAssignToTeam(team.getTeamId(), memberIds); + + assertEquals(1, results.size()); + assertTrue(isMemberOfService.isMemberOfTeam(member2.getAccountId(), team.getTeamId())); + } @Test void testRemoveMemberFromTeam() { diff --git a/frontend/react-app/src/api/isMemberOfApi.js b/frontend/react-app/src/api/isMemberOfApi.js index 89b4393..0f89f79 100644 --- a/frontend/react-app/src/api/isMemberOfApi.js +++ b/frontend/react-app/src/api/isMemberOfApi.js @@ -14,6 +14,29 @@ export const addMemberToTeam = async (teamMemberId, teamId) => { return true; }; +//Adding multiple members to a team at once +export const massAssignToTeam = async (teamId, teamMemberIds) => { + try { + console.log(JSON.stringify(teamMemberIds)); + const response = await fetch(`${BASE_URL}/team/${teamId}/mass-assign`, { + method: 'POST', + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(teamMemberIds) + }); + + if (!response.ok) { + console.error(`Failed to assign members to team: ${response.status} ${response.statusText}`); + return null; + } + + return await response.json(); + } + catch (error) { + console.error("Error assigning members to team: ", error); + throw error; + } +}; + //Remove a member from a team export const removeMemberFromTeam = async (teamMemberId, teamId) => { try { diff --git a/frontend/react-app/src/tests/api-tests/isMemberOfApiTest.test.js b/frontend/react-app/src/tests/api-tests/isMemberOfApiTest.test.js index de65527..efd55f9 100644 --- a/frontend/react-app/src/tests/api-tests/isMemberOfApiTest.test.js +++ b/frontend/react-app/src/tests/api-tests/isMemberOfApiTest.test.js @@ -1,4 +1,4 @@ -import { addMemberToTeam, removeMemberFromTeam, checkIfAssignedToTeam } from '../../api/isMemberOfApi'; +import { addMemberToTeam, removeMemberFromTeam, checkIfAssignedToTeam, massAssignToTeam } from '../../api/isMemberOfApi'; const BASE_URL = "http://localhost:8080/api/memberships"; @@ -20,6 +20,29 @@ describe('IsMemberOf API', () => { expect(result).toBe(true); }); + //test: adding multiple members to a team + test('massAssignToTeam should return team assignment data on sucess', async () => { + const mockResponse = [ + { "isMemberOfId": 101, "teamMemberId": 1, "teamId": 42 }, + { "isMemberOfId": 102, "teamMemberId": 3, "teamId": 42 }, + { "isMemberOfId": 103, "teamMemberId": 4, "teamId": 42 } + ]; + + const teamMemberIds = [1, 3, 4]; + + fetch.mockResponseOnce(JSON.stringify(mockResponse), { status: 200 }); + + const result = await massAssignToTeam(2, teamMemberIds); + + expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/team/2/mass-assign`, { + method: 'POST', + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(teamMemberIds) + }); + + expect(result).toEqual(mockResponse); + }); + //test: removing a member from a team test('removeMemberFromTeam should return true on success', async () => { fetch.mockResponseOnce('', { status: 200 });