Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,28 +24,37 @@
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;
import in.koreatech.koin.acceptance.fixture.DepartmentAcceptanceFixture;
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 {

Expand All @@ -59,6 +70,12 @@ class TeamRecruitmentApplicationFlowApiTest extends AcceptanceTest {
@Autowired
private TeamRecruitmentProfileRepository profileRepository;

@Autowired
private TeamRecruitmentRoleRepository roleRepository;

@Autowired
private StudentRepository studentRepository;

@Autowired
private TeamRecruitmentApplicationRepository applicationRepository;

Expand All @@ -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());
Expand All @@ -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<TeamRecruitmentRole> 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<TeamRecruitmentChatRoom> 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();
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
Loading