From 01720d204eec36e528564c92a31609133741ab1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=ED=83=9C=EC=A7=84?= <140797244+taejinn@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:03:07 +0900 Subject: [PATCH] =?UTF-8?q?test:=20ROLE=5FBASED=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EC=97=AC=EC=A0=95=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...TeamRecruitmentApplicationFlowApiTest.java | 212 +++++++++++++++++- 1 file changed, 211 insertions(+), 1 deletion(-) diff --git a/src/test/java/in/koreatech/koin/acceptance/domain/TeamRecruitmentApplicationFlowApiTest.java b/src/test/java/in/koreatech/koin/acceptance/domain/TeamRecruitmentApplicationFlowApiTest.java index b9d8ca80bd..e2d9cc6f1e 100644 --- a/src/test/java/in/koreatech/koin/acceptance/domain/TeamRecruitmentApplicationFlowApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/domain/TeamRecruitmentApplicationFlowApiTest.java @@ -12,6 +12,8 @@ import static in.koreatech.koin.domain.team.recruitment.enums.TeamRecruitmentStatus.CLOSED; import static in.koreatech.koin.domain.team.recruitment.enums.TeamRecruitmentStatus.RECRUITING; import static in.koreatech.koin.domain.team.recruitment.enums.TeamRecruitmentType.GENERAL; +import static in.koreatech.koin.domain.user.model.UserIdentity.UNDERGRADUATE; +import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.nullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -22,11 +24,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.LocalDate; +import java.util.List; +import java.util.stream.StreamSupport; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import in.koreatech.koin.acceptance.AcceptanceTest; @@ -34,16 +41,20 @@ import in.koreatech.koin.acceptance.fixture.UserAcceptanceFixture; import in.koreatech.koin.domain.student.model.Department; import in.koreatech.koin.domain.student.model.Student; +import in.koreatech.koin.domain.student.repository.StudentRepository; import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitment; import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitmentApplication; import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitmentChatMember; import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitmentChatRoom; import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitmentProfile; +import in.koreatech.koin.domain.team.recruitment.model.TeamRecruitmentRole; import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentApplicationRepository; import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentChatMemberRepository; import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentChatRoomRepository; import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentProfileRepository; import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentRepository; +import in.koreatech.koin.domain.team.recruitment.repository.TeamRecruitmentRoleRepository; +import in.koreatech.koin.domain.user.model.User; class TeamRecruitmentApplicationFlowApiTest extends AcceptanceTest { @@ -59,6 +70,12 @@ class TeamRecruitmentApplicationFlowApiTest extends AcceptanceTest { @Autowired private TeamRecruitmentProfileRepository profileRepository; + @Autowired + private TeamRecruitmentRoleRepository roleRepository; + + @Autowired + private StudentRepository studentRepository; + @Autowired private TeamRecruitmentApplicationRepository applicationRepository; @@ -68,16 +85,20 @@ class TeamRecruitmentApplicationFlowApiTest extends AcceptanceTest { @Autowired private TeamRecruitmentChatMemberRepository chatMemberRepository; + @Autowired + private ObjectMapper objectMapper; + private Student author; private Student applicant; private String authorToken; private String applicantToken; + private Department department; private RecruitmentContext recruitmentContext; @BeforeEach void setUp() { clear(); - Department department = departmentFixture.컴퓨터공학부(); + department = departmentFixture.컴퓨터공학부(); author = userFixture.준호_학생(department, null); applicant = userFixture.성빈_학생(department); authorToken = userFixture.getToken(author.getUser()); @@ -91,6 +112,135 @@ void setUp() { recruitmentContext = saveRecruitmentAndTeamRoom("팀원 모집", 2); } + @Test + void ROLE_BASED_모집은_지원한_역할만_정원에_따라_마감한다() throws Exception { + MvcResult creationResult = mockMvc.perform(post("/team-recruitments") + .header("Authorization", "Bearer " + authorToken) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "category": "PROJECT", + "title": "ROLE_BASED 지원 여정", + "meeting_type": "ONLINE", + "activity_start_date": "%s", + "activity_end_date": "%s", + "deadline_date": "%s", + "recruitment_type": "ROLE_BASED", + "roles": [ + {"name": "백엔드", "max_participants": 1}, + {"name": "프론트엔드", "max_participants": 1} + ], + "description": "역할 기반 모집" + } + """.formatted( + LocalDate.now(clock).plusDays(2), + LocalDate.now(clock).plusDays(10), + LocalDate.now(clock).plusDays(1) + ))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNumber()) + .andReturn(); + + Integer recruitmentId = objectMapper + .readTree(creationResult.getResponse().getContentAsString()) + .path("id") + .asInt(); + assertThat(recruitmentId).isPositive(); + + TeamRecruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow(); + assertThat(recruitment.getRecruitmentType()).isEqualTo( + in.koreatech.koin.domain.team.recruitment.enums.TeamRecruitmentType.ROLE_BASED + ); + List roles = roleRepository + .findAllByRecruitment_IdOrderByDisplayOrderAsc(recruitmentId); + assertThat(roles).hasSize(2); + TeamRecruitmentRole role = roles.stream() + .filter(candidate -> candidate.getName().equals("백엔드")) + .findFirst() + .orElseThrow(); + TeamRecruitmentRole frontendRole = roles.stream() + .filter(candidate -> candidate.getName().equals("프론트엔드")) + .findFirst() + .orElseThrow(); + assertThat(role.getId()).isNotEqualTo(frontendRole.getId()); + + List chatRooms = chatRoomRepository.findAllByRecruitment_Id(recruitmentId); + assertThat(chatRooms).filteredOn(chatRoom -> chatRoom.getRoomType() == TEAM).hasSize(1); + TeamRecruitmentChatRoom teamRoom = chatRooms.stream() + .filter(chatRoom -> chatRoom.getRoomType() == TEAM) + .findFirst() + .orElseThrow(); + assertThat(teamRoom.getRecruitment().getId()).isEqualTo(recruitmentId); + + applyForRole(recruitment, role, applicantToken, "첫 지원자") + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.role.id").value(role.getId())) + .andExpect(jsonPath("$.role.name").value("백엔드")); + TeamRecruitmentApplication rejectedApplication = applicationRepository + .findByRecruitment_IdAndApplicant_Id(recruitment.getId(), applicant.getUser().getId()) + .orElseThrow(); + reject(recruitment, rejectedApplication, authorToken) + .andExpect(status().isNoContent()); + + Student acceptedApplicant = saveSecondApplicant(); + String acceptedApplicantToken = userFixture.getToken(acceptedApplicant.getUser()); + saveProfile(acceptedApplicant, "승인 지원자"); + applyForRole(recruitment, role, acceptedApplicantToken, "두 번째 지원자") + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.role.id").value(role.getId())) + .andExpect(jsonPath("$.role.name").value("백엔드")); + TeamRecruitmentApplication acceptedApplication = applicationRepository + .findByRecruitment_IdAndApplicant_Id(recruitment.getId(), acceptedApplicant.getUser().getId()) + .orElseThrow(); + accept(recruitment, acceptedApplication, authorToken) + .andExpect(status().isNoContent()); + + entityManager.flush(); + entityManager.clear(); + + MvcResult detailResult = mockMvc.perform(get("/team-recruitments/{id}", recruitmentId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(recruitmentId)) + .andExpect(jsonPath("$.recruitment_type").value("ROLE_BASED")) + .andExpect(jsonPath("$.status").value("RECRUITING")) + .andExpect(jsonPath("$.current_participants").value(1)) + .andExpect(jsonPath("$.max_participants").value(2)) + .andReturn(); + + JsonNode detail = objectMapper.readTree(detailResult.getResponse().getContentAsString()); + assertThat(detail.path("roles").size()).isEqualTo(2); + assertRole(detail, role.getId(), "백엔드", 1, 1, true); + assertRole(detail, frontendRole.getId(), "프론트엔드", 0, 1, false); + + assertThat(applicationRepository.findById(rejectedApplication.getId()).orElseThrow().getStatus()) + .isEqualTo(REJECTED); + assertThat(applicationRepository.findById(acceptedApplication.getId()).orElseThrow().getStatus()) + .isEqualTo(ACCEPTED); + assertThat(chatRoomRepository.findById(teamRoom.getId()).orElseThrow().getStatus()).isEqualTo(ACTIVE); + assertThat(chatMemberRepository.findAllByChatRoom_Id(teamRoom.getId())) + .extracting(member -> member.getUser().getId()) + .containsExactlyInAnyOrder(author.getUser().getId(), acceptedApplicant.getUser().getId()); + } + + private void assertRole( + JsonNode recruitmentDetail, + Integer roleId, + String roleName, + int currentParticipants, + int maxParticipants, + boolean closed + ) { + JsonNode role = StreamSupport.stream(recruitmentDetail.path("roles").spliterator(), false) + .filter(candidate -> candidate.path("id").asInt() == roleId) + .findFirst() + .orElseThrow(() -> new AssertionError("응답에서 role id를 찾을 수 없습니다: " + roleId)); + assertThat(role.path("id").asInt()).isEqualTo(roleId); + assertThat(role.path("name").asText()).isEqualTo(roleName); + assertThat(role.path("current_participants").asInt()).isEqualTo(currentParticipants); + assertThat(role.path("max_participants").asInt()).isEqualTo(maxParticipants); + assertThat(role.path("is_closed").asBoolean()).isEqualTo(closed); + } + @Test void 모집글과_TEAM_방이_생성된_상태에서_지원하고_승인하면_TEAM_방에_참여한다() throws Exception { TeamRecruitment recruitment = recruitmentContext.recruitment(); @@ -552,6 +702,24 @@ private ResultActions apply( """)); } + private ResultActions applyForRole( + TeamRecruitment recruitment, + TeamRecruitmentRole role, + String token, + String motivation + ) throws Exception { + return mockMvc.perform(post("/team-recruitments/{recruitmentId}/applications", recruitment.getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "role_id": %d, + "motivation": "%s", + "availability": "평일 저녁" + } + """.formatted(role.getId(), motivation))); + } + private ResultActions accept( TeamRecruitment recruitment, TeamRecruitmentApplication application, @@ -568,6 +736,48 @@ private ResultActions accept( """)); } + private ResultActions reject( + TeamRecruitment recruitment, + TeamRecruitmentApplication application, + String token + ) throws Exception { + return mockMvc.perform(put("/team-recruitments/{recruitmentId}/applications/{applicationId}/status", + recruitment.getId(), application.getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "status": "REJECTED" + } + """)); + } + + private Student saveSecondApplicant() { + return studentRepository.save(Student.builder() + .studentNumber("2023999999") + .department(department) + .userIdentity(UNDERGRADUATE) + .isGraduated(false) + .user(User.builder() + .loginPw("test-password") + .name("ROLE_BASED 승인 지원자") + .anonymousNickname("익명_역할지원자") + .userType(STUDENT) + .isAuthed(true) + .isDeleted(false) + .build()) + .build()); + } + + private void saveProfile(Student student, String nickname) { + profileRepository.save(TeamRecruitmentProfile.builder() + .user(student.getUser()) + .profileNickname(nickname) + .preferredRole("백엔드") + .selfIntroduction("ROLE_BASED 지원자") + .build()); + } + private RecruitmentContext saveRecruitmentAndTeamRoom(String title, int maxParticipants) { TeamRecruitment recruitment = saveRecruitment(title, maxParticipants); TeamRecruitmentChatRoom teamRoom = chatRoomRepository.save(TeamRecruitmentChatRoom.builder()