diff --git a/ddl.sql b/ddl.sql index 587808a9..a9a09c43 100644 --- a/ddl.sql +++ b/ddl.sql @@ -3,6 +3,7 @@ create schema post; create schema tag; create schema score; create schema employer; +create schema interview; create sequence post.memo_series_order_seq start 1; CREATE TABLE tag.memo_to_tag ( @@ -283,3 +284,17 @@ create table score.info ( created_date timestamp default current_timestamp, primary key (user_id,score_type,post_id) ); + +create table interview.info( + id bigserial primary key, + employer_id int8, + employee_id int8, + status varchar(8) default 'READY', ---'DONE', 'ING_Q1','ING_Q2','ING_Q3', 'READY' + question_1 varchar, + answer_1 varchar, + question_2 varchar, + answer_2 varchar, + question_3 varchar, + answer_3 varchar, + unique (employer_id, employee_id) +); diff --git a/src/main/java/Funssion/Inforum/common/exception/forbidden/ForbiddenException.java b/src/main/java/Funssion/Inforum/common/exception/forbidden/ForbiddenException.java index 5559382b..a2b854c8 100644 --- a/src/main/java/Funssion/Inforum/common/exception/forbidden/ForbiddenException.java +++ b/src/main/java/Funssion/Inforum/common/exception/forbidden/ForbiddenException.java @@ -12,12 +12,12 @@ public class ForbiddenException extends RuntimeException{ public ForbiddenException(String message) { this.message = message; - this.errorResult = new ErrorResult(HttpStatus.NOT_FOUND, message); + this.errorResult = new ErrorResult(HttpStatus.FORBIDDEN, message); } public ForbiddenException(String message, Throwable cause) { super(cause); this.message = message; - this.errorResult = new ErrorResult(HttpStatus.NOT_FOUND, message); + this.errorResult = new ErrorResult(HttpStatus.FORBIDDEN, message); } } diff --git a/src/main/java/Funssion/Inforum/config/SecurityConfig.java b/src/main/java/Funssion/Inforum/config/SecurityConfig.java index a655d379..b34501f6 100644 --- a/src/main/java/Funssion/Inforum/config/SecurityConfig.java +++ b/src/main/java/Funssion/Inforum/config/SecurityConfig.java @@ -72,6 +72,9 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti "/users/check-duplication").permitAll() .requestMatchers(HttpMethod.GET, "/users/profile/**").permitAll() .requestMatchers("/employer/**").hasRole("EMPLOYER") + .requestMatchers(HttpMethod.POST,"/interview/questions/**").hasRole("EMPLOYER") + .requestMatchers(HttpMethod.GET,"/interview/answers/**").hasAnyRole("EMPLOYER","USER") + .requestMatchers(HttpMethod.POST,"/interview/answers/**").hasRole("USER") .requestMatchers(HttpMethod.GET, "/score/**").permitAll() .requestMatchers(HttpMethod.GET,"/score/rank/**").authenticated() .requestMatchers(HttpMethod.POST, "/users/login").authenticated() //spring security filter에서 redirect diff --git a/src/main/java/Funssion/Inforum/domain/interview/constant/InterviewStatus.java b/src/main/java/Funssion/Inforum/domain/interview/constant/InterviewStatus.java new file mode 100644 index 00000000..7bfe4d58 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/constant/InterviewStatus.java @@ -0,0 +1,15 @@ +package Funssion.Inforum.domain.interview.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +@Getter +@RequiredArgsConstructor +public enum InterviewStatus { + READY("면접 시작 전입니다."), + ING_Q1("1번 문제를 푸는 중입니다."), + ING_Q2("2번 문제를 푸는 중입니다."), + ING_Q3("3번 문제를 푸는 중입니다."), + DONE("면접이 완료 되었습니다."); + private final String status; + +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/controller/InterviewController.java b/src/main/java/Funssion/Inforum/domain/interview/controller/InterviewController.java new file mode 100644 index 00000000..e51b1e6f --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/controller/InterviewController.java @@ -0,0 +1,45 @@ +package Funssion.Inforum.domain.interview.controller; + +import Funssion.Inforum.common.dto.IsSuccessResponseDto; +import Funssion.Inforum.common.utils.SecurityContextUtils; +import Funssion.Inforum.domain.interview.constant.InterviewStatus; +import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto; +import Funssion.Inforum.domain.interview.dto.InterviewQuestionDto; +import Funssion.Inforum.domain.interview.dto.QuestionsDto; +import Funssion.Inforum.domain.interview.exception.InterviewForbiddenException; +import Funssion.Inforum.domain.interview.service.InterviewService; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/interview") +@RequiredArgsConstructor +public class InterviewController { + private final InterviewService interviewService; + @PostMapping("/questions/{employeeId}") + public IsSuccessResponseDto saveInterviewQuestions(@PathVariable Long employeeId, @RequestBody @Valid QuestionsDto questionsDto){ + return interviewService.saveQuestionsAndNotifyInterview(employeeId,questionsDto); + } + @GetMapping("/questions/{employerId}/{employeeId}") + public InterviewQuestionDto getInterviewQuestion(@PathVariable Long employerId, @PathVariable Long employeeId){ + return interviewService.getInterviewInfoTo(employeeId, employerId); + } + @ApiResponse(description="1번 문제를 봤을 때 호출됩니다. 이에 따라 status가 1번문제 보는중으로 바뀜") + @PutMapping("/start/{employerId}") + public InterviewStatus startInterviewByEmployee(@PathVariable Long employerId){ + Long userId = SecurityContextUtils.getAuthorizedUserId(); + + return interviewService.startInterview(employerId,userId); + } + @ApiResponse(description="답변을 제출할때마다 호출됩니다. 이에따라 다음문제 보는중으로 바뀜. ex) 1번 답변 제출시 2번문제 보는중으로 바뀜") + @PostMapping("/answers") + public IsSuccessResponseDto saveAnswerOfQuestion(@RequestBody InterviewAnswerDto interviewAnswerDto){ + Long userId = SecurityContextUtils.getAuthorizedUserId(); + if(!interviewService.authorizeInterview(interviewAnswerDto.getEmployerId(),userId)) throw new InterviewForbiddenException(); + + interviewService.saveAnswerOfQuestion(interviewAnswerDto,userId); + return new IsSuccessResponseDto(true, interviewAnswerDto.getQuestionNumber() + "번 질문의 답변이 성공적으로 등록되었습니다."); + } +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/domain/Interview.java b/src/main/java/Funssion/Inforum/domain/interview/domain/Interview.java new file mode 100644 index 00000000..19cb5943 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/domain/Interview.java @@ -0,0 +1,18 @@ +package Funssion.Inforum.domain.interview.domain; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@EqualsAndHashCode +@Builder +public class Interview { + private final Long employerId; + private final String status; + private final String question1; + private final String question2; + private final String question3; +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewAnswerDto.java b/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewAnswerDto.java new file mode 100644 index 00000000..525e0df5 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewAnswerDto.java @@ -0,0 +1,14 @@ +package Funssion.Inforum.domain.interview.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class InterviewAnswerDto { + private Long employerId; + private Integer questionNumber; + private String answer; +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewQuestionDto.java b/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewQuestionDto.java new file mode 100644 index 00000000..69ecbe39 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/dto/InterviewQuestionDto.java @@ -0,0 +1,23 @@ +package Funssion.Inforum.domain.interview.dto; + +import Funssion.Inforum.domain.interview.domain.Interview; +import lombok.Getter; + +@Getter +public class InterviewQuestionDto { + private final String question1; + private final String question2; + private final String question3; + private final String status; + private final Long employerId; + private String companyName; + public InterviewQuestionDto(Interview interviewQuestion, String companyName){ + this.question1 = interviewQuestion.getQuestion1(); + this.question2 = interviewQuestion.getQuestion2(); + this.question3 = interviewQuestion.getQuestion3(); + this.status = interviewQuestion.getStatus(); + this.employerId = interviewQuestion.getEmployerId(); + this.companyName = companyName; + + } +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/dto/QuestionsDto.java b/src/main/java/Funssion/Inforum/domain/interview/dto/QuestionsDto.java new file mode 100644 index 00000000..b6429c46 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/dto/QuestionsDto.java @@ -0,0 +1,18 @@ +package Funssion.Inforum.domain.interview.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class QuestionsDto { + @NotEmpty + private String question1; + @NotEmpty + private String question2; + @NotEmpty + private String question3; +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/exception/InterviewForbiddenException.java b/src/main/java/Funssion/Inforum/domain/interview/exception/InterviewForbiddenException.java new file mode 100644 index 00000000..b4ec3bd4 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/exception/InterviewForbiddenException.java @@ -0,0 +1,9 @@ +package Funssion.Inforum.domain.interview.exception; + +import Funssion.Inforum.common.exception.forbidden.ForbiddenException; + +public class InterviewForbiddenException extends ForbiddenException { + public InterviewForbiddenException() { + super("해당 유저에게 할당된 인터뷰가 아닙니다."); + } +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/repository/InterviewRepository.java b/src/main/java/Funssion/Inforum/domain/interview/repository/InterviewRepository.java new file mode 100644 index 00000000..3a9d3bd0 --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/repository/InterviewRepository.java @@ -0,0 +1,117 @@ +package Funssion.Inforum.domain.interview.repository; + +import Funssion.Inforum.common.exception.badrequest.BadRequestException; +import Funssion.Inforum.common.utils.SecurityContextUtils; +import Funssion.Inforum.domain.interview.constant.InterviewStatus; +import Funssion.Inforum.domain.interview.domain.Interview; +import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto; +import Funssion.Inforum.domain.interview.dto.QuestionsDto; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; + +@Repository +public class InterviewRepository { + private final JdbcTemplate template; + public InterviewRepository(DataSource dataSource){ + this.template = new JdbcTemplate(dataSource); + } + + public void saveQuestions(Long employeeId, QuestionsDto questionsDto){ + Long employerId = SecurityContextUtils.getAuthorizedUserId(); + + String sql = "INSERT INTO interview.info (employer_id, employee_id, question_1, question_2, question_3) values(?,?,?,?,?)"; + template.update(sql,employerId,employeeId, questionsDto.getQuestion1(),questionsDto.getQuestion2(),questionsDto.getQuestion3()); + } + + public boolean findIfAlreadyInterviewing(Long employeeId){ + Long employerId = SecurityContextUtils.getAuthorizedUserId(); + String sql = + "SELECT EXISTS " + + "(SELECT 1 " + + "FROM interview.info " + + "WHERE employer_id = ? AND employee_id = ? )"; + return template.queryForObject(sql,Boolean.class,employerId,employeeId); + } + public Interview getInterviewQuestionOf(Long employeeId, Long employerId){ + String sql = + "SELECT employer_id, status, question_1, question_2, question_3 " + + "FROM interview.info " + + "WHERE employee_id = ? and employer_id = ?"; + return template.queryForObject(sql,interviewRowMapper(),employeeId, employerId); + } + + public void saveAnswerOfQuestion(InterviewAnswerDto interviewAnswerDto,Long userId) { + String columnNameOfAnswer = switch (interviewAnswerDto.getQuestionNumber()) { + case 1 -> "answer_1"; + case 2 -> "answer_2"; + case 3 -> "answer_3"; + default -> throw new BadRequestException("인터뷰 답변객체의 번호가 '1','2','3' 이 아닙니다."); + }; + + String sql = + "UPDATE interview.info " + + "SET " + columnNameOfAnswer + " = ? " + + "WHERE employer_id = ? and employee_id = ?"; + + template.update(sql,interviewAnswerDto.getAnswer(), interviewAnswerDto.getEmployerId(),userId); + } + + public InterviewStatus updateStatus(Long employerId, Long employeeId, InterviewStatus interviewStatus){ + String sql = + "UPDATE interview.info " + + "SET status = ? " + + "WHERE employer_id = ? and employee_id = ?"; + template.update(sql,interviewStatus.toString(), employerId, employeeId); + return getInterviewStatusOfUser(employerId, employeeId); + } + public InterviewStatus startInterview(Long employerId, Long employeeId) { + String sql = + "UPDATE interview.info " + + "SET status = ? " + + "WHERE employer_id = ? and employee_id = ?"; + template.update(sql, InterviewStatus.ING_Q1.toString(),employerId,employeeId); + + return getInterviewStatusOfUser(employerId,employeeId); + } + + public InterviewStatus getInterviewStatusOfUser(Long employerId, Long userId){ + String sql = + "SELECT status " + + "FROM interview.info " + + "WHERE employer_id = ? and employee_id = ?"; + return InterviewStatus.valueOf(template.queryForObject(sql,String.class, employerId, userId)); + } + public Boolean isAuthorizedInterview(Long employerId,Long employeeId) { + String sql = + "SELECT EXISTS(" + + "SELECT employee_id " + + "FROM interview.info " + + "WHERE employer_id = ? AND employee_id = ?)"; + return template.queryForObject(sql, Boolean.class, employerId,employeeId); + } + private RowMapper interviewRowMapper(){ + return (rs,rowNum)-> + Interview.builder() + .employerId(rs.getLong("employer_id")) + .status(rs.getString("status")) + .question1(rs.getString("question_1")) + .question2(rs.getString("question_2")) + .question3(rs.getString("question_3")) + .build(); + } + + public boolean isMismatchWithStatus(InterviewAnswerDto interviewAnswerDto, Long userId) { + InterviewStatus interviewStatusOfUser = getInterviewStatusOfUser(interviewAnswerDto.getEmployerId(), userId); + if (isStatusMismatchWithQuestionNumber(interviewAnswerDto, interviewStatusOfUser)) return true; + return false; + } + + private static boolean isStatusMismatchWithQuestionNumber(InterviewAnswerDto interviewAnswerDto, InterviewStatus interviewStatusOfUser) { + return !interviewStatusOfUser.getStatus().startsWith(String.valueOf(interviewAnswerDto.getQuestionNumber())); + } + + +} diff --git a/src/main/java/Funssion/Inforum/domain/interview/service/InterviewService.java b/src/main/java/Funssion/Inforum/domain/interview/service/InterviewService.java new file mode 100644 index 00000000..2b2c0a2e --- /dev/null +++ b/src/main/java/Funssion/Inforum/domain/interview/service/InterviewService.java @@ -0,0 +1,69 @@ +package Funssion.Inforum.domain.interview.service; + +import Funssion.Inforum.common.dto.IsSuccessResponseDto; +import Funssion.Inforum.common.exception.badrequest.BadRequestException; +import Funssion.Inforum.common.exception.etc.DuplicateException; +import Funssion.Inforum.domain.interview.constant.InterviewStatus; +import Funssion.Inforum.domain.interview.domain.Interview; +import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto; +import Funssion.Inforum.domain.interview.dto.InterviewQuestionDto; +import Funssion.Inforum.domain.interview.dto.QuestionsDto; +import Funssion.Inforum.domain.interview.exception.InterviewForbiddenException; +import Funssion.Inforum.domain.interview.repository.InterviewRepository; +import Funssion.Inforum.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class InterviewService { + private final InterviewRepository interviewRepository; + private final MemberRepository memberRepository; + public IsSuccessResponseDto saveQuestionsAndNotifyInterview(Long employeeId, QuestionsDto questionsDto){ + if(interviewRepository.findIfAlreadyInterviewing(employeeId)) throw new DuplicateException("이미 면접요청을 보낸 지원자입니다."); + + interviewRepository.saveQuestions(employeeId,questionsDto); + // ToDo : notification + + return new IsSuccessResponseDto(true, "성공적으로 면접알림을 전송하였습니다."); + } + + + public InterviewQuestionDto getInterviewInfoTo(Long employeeId, Long employerId){ + Interview interviewInfo = interviewRepository.getInterviewQuestionOf(employeeId, employerId); + return new InterviewQuestionDto(interviewInfo,memberRepository.getCompanyName(interviewInfo.getEmployerId())); + } + + public InterviewStatus startInterview(Long employerId, Long employeeId){ + if (!authorizeInterview(employerId,employeeId)) throw new InterviewForbiddenException(); + + return interviewRepository.startInterview(employerId, employeeId); + } + public InterviewStatus saveAnswerOfQuestion(InterviewAnswerDto interviewAnswerDto, Long userId) { + // 풀고있는 상태와 다른 답변 문제가 제출될 경우의 에러처리 + handlingOfMismatchWithStatus(interviewAnswerDto,userId); + + interviewRepository.saveAnswerOfQuestion(interviewAnswerDto,userId); + + InterviewStatus interviewStatusAfterAnswer = getInterviewStatus(interviewAnswerDto); + return interviewRepository.updateStatus(interviewAnswerDto.getEmployerId(),userId,interviewStatusAfterAnswer); + + } + + private static InterviewStatus getInterviewStatus(InterviewAnswerDto interviewAnswerDto) { + InterviewStatus interviewStatusAfterAnswer = switch (interviewAnswerDto.getQuestionNumber()) { + case 1 -> InterviewStatus.ING_Q2; + case 2 -> InterviewStatus.ING_Q3; + case 3 -> InterviewStatus.DONE; + default -> throw new BadRequestException("인터뷰 답변객체의 번호가 '1','2','3' 이 아닙니다."); + }; + return interviewStatusAfterAnswer; + } + + public Boolean authorizeInterview(Long employerId,Long employeeId){ + return interviewRepository.isAuthorizedInterview(employerId,employeeId); + } + private void handlingOfMismatchWithStatus(InterviewAnswerDto interviewAnswerDto,Long userId){ + if (interviewRepository.isMismatchWithStatus(interviewAnswerDto,userId)) throw new BadRequestException("잘못된 문제로 요청을 보내고 있습니다. 현 문제번호와 답변을 확인해주세요."); + } +} diff --git a/src/main/java/Funssion/Inforum/domain/member/dto/response/SaveMemberResponseDto.java b/src/main/java/Funssion/Inforum/domain/member/dto/response/SaveMemberResponseDto.java index eb737597..2134a855 100644 --- a/src/main/java/Funssion/Inforum/domain/member/dto/response/SaveMemberResponseDto.java +++ b/src/main/java/Funssion/Inforum/domain/member/dto/response/SaveMemberResponseDto.java @@ -20,4 +20,5 @@ public class SaveMemberResponseDto { private final LocalDateTime createdDate; private final String email; private final String role; + private String companyName; } diff --git a/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepository.java b/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepository.java index 48783658..a188fda6 100644 --- a/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepository.java +++ b/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepository.java @@ -37,4 +37,6 @@ public interface MemberRepository { void deleteUser(Long userId); Long getDailyScore(Long userId); + + String getCompanyName(Long userId); } diff --git a/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepositoryImpl.java b/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepositoryImpl.java index 2ca79204..305d8010 100644 --- a/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepositoryImpl.java +++ b/src/main/java/Funssion/Inforum/domain/member/repository/MemberRepositoryImpl.java @@ -265,6 +265,15 @@ public Long getDailyScore(Long userId) { return jdbcTemplate.queryForObject(sql,Long.class,userId); } + @Override + public String getCompanyName(Long userId) { + String sql = + "SELECT company " + + "FROM member.info " + + "WHERE id = ?"; + return jdbcTemplate.queryForObject(sql,String.class,userId); + } + private RowMapper nonSocialmemberRowMapper(){ return new RowMapper() { @Override diff --git a/src/test/java/Funssion/Inforum/domain/interview/InterviewIntegrationTest.java b/src/test/java/Funssion/Inforum/domain/interview/InterviewIntegrationTest.java new file mode 100644 index 00000000..d8380be6 --- /dev/null +++ b/src/test/java/Funssion/Inforum/domain/interview/InterviewIntegrationTest.java @@ -0,0 +1,225 @@ +package Funssion.Inforum.domain.interview; + +import Funssion.Inforum.domain.employer.repository.EmployerRepository; +import Funssion.Inforum.domain.interview.constant.InterviewStatus; +import Funssion.Inforum.domain.interview.domain.Interview; +import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto; +import Funssion.Inforum.domain.interview.dto.QuestionsDto; +import Funssion.Inforum.domain.interview.repository.InterviewRepository; +import Funssion.Inforum.domain.member.constant.LoginType; +import Funssion.Inforum.domain.member.dto.request.EmployerSaveDto; +import Funssion.Inforum.domain.member.dto.request.MemberSaveDto; +import Funssion.Inforum.domain.member.dto.response.SaveMemberResponseDto; +import Funssion.Inforum.domain.member.entity.NonSocialMember; +import Funssion.Inforum.domain.member.repository.MemberRepository; +import Funssion.Inforum.domain.member.service.AuthService; +import Funssion.Inforum.domain.notification.repository.NotificationRepository; +import Funssion.Inforum.domain.professionalprofile.dto.request.CreateProfessionalProfileDto; +import Funssion.Inforum.domain.professionalprofile.repository.ProfessionalProfileRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +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.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class InterviewIntegrationTest { + @Autowired + MockMvc mvc; + @Autowired + AuthService authService; + @Autowired + MemberRepository memberRepository; + @Autowired + EmployerRepository employerRepository; + @Autowired + NotificationRepository notificationRepository; + @Autowired + InterviewRepository interviewRepository; + @Autowired + ProfessionalProfileRepository professionalProfileRepository; + + ObjectMapper objectMapper = new ObjectMapper(); + + SaveMemberResponseDto saveEmployeeDto; + SaveMemberResponseDto saveEmployerDto; + + CreateProfessionalProfileDto createdProfessionalProfileDto = CreateProfessionalProfileDto.builder() + .introduce("hi") + .techStack("{\"java\": 5}") + .description("java gosu") + .answer1("yes") + .answer2("no") + .answer3("good") + .resume("{\"content\": \"i'm a amazing programmer\"}") + .build(); + QuestionsDto questionsDto = new QuestionsDto("1번 질문","2번 질문", "3번 질문"); + + + + @BeforeEach + void init() { + saveEmployeeDto = memberRepository.save( + NonSocialMember.createNonSocialMember( + new MemberSaveDto("employee1", LoginType.NON_SOCIAL, "test1@gmail.com", "a1234567!")) + ); + saveEmployerDto = memberRepository.save(EmployerSaveDto.builder() + .userName("Mock(향로)") + .loginType(LoginType.NON_SOCIAL) + .userEmail("employer@gmail.com") + .userPw("a1234567!") + .companyName("inflearn") + .build()); + + professionalProfileRepository.create( + saveEmployeeDto.getId(), + createdProfessionalProfileDto); + } + + @Nested + @DisplayName("면접 요청") + class requestInterview{ + @Test + @DisplayName("지원자에게 질문과 함께 면접을 요청합니다.") + void requestInterviewWithQuestions() throws Exception { + memberRepository.authorizeEmployer(saveEmployerDto.getId()); + UserDetails authorizedEmployerUserDetails = authService.loadUserByUsername(saveEmployerDto.getEmail()); + + + mvc.perform(post("/interview/questions/" + saveEmployeeDto.getId()) + .with(user(authorizedEmployerUserDetails)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(questionsDto))); + Interview interviewQuestionOf = interviewRepository.getInterviewQuestionOf(saveEmployeeDto.getId(), saveEmployerDto.getId()); + assertThat(interviewQuestionOf.equals(new Interview(saveEmployerDto.getId(), InterviewStatus.READY.toString(), questionsDto.getQuestion1(), questionsDto.getQuestion2(), questionsDto.getQuestion3()))); + } + + @Test + @DisplayName("지원자에게 면접요청을 하고나서 중복해서 요청을 보낼 수 없습니다.") + void requestInterviewAgainBlocked() throws Exception { + memberRepository.authorizeEmployer(saveEmployerDto.getId()); + UserDetails authorizedEmployerUserDetails = authService.loadUserByUsername(saveEmployerDto.getEmail()); + mvc.perform(post("/interview/questions/" + saveEmployeeDto.getId()) + .with(user(authorizedEmployerUserDetails)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(questionsDto))); + + MvcResult result = mvc.perform(post("/interview/questions/" + saveEmployeeDto.getId()) + .with(user(authorizedEmployerUserDetails)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(questionsDto))) + .andExpect(status().isConflict()) + .andReturn(); + assertThat(result.getResolvedException().getMessage().equals("이미 면접요청을 보낸 지원자입니다.")); + } + + @Test + @DisplayName("일반 유저가 면접을 요청할 수 없습니다.") + void requestInterviewForbiddenByUser() throws Exception { + UserDetails employee = authService.loadUserByUsername(saveEmployeeDto.getEmail()); + mvc.perform(post("/interview/questions/" + saveEmployeeDto.getId()) + .with(user(employee)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(questionsDto))) + .andExpect(status().isForbidden()); + } + @Test + @DisplayName("인증받지 않은 채용자가 면접을 요청할 수 없습니다.") + void requestInterviewForbiddenByUnauthorizedEmployer() throws Exception { + UserDetails unAuthorizedEmployee = authService.loadUserByUsername(saveEmployerDto.getEmail()); + mvc.perform(post("/interview/questions/" + saveEmployeeDto.getId()) + .with(user(unAuthorizedEmployee)) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(questionsDto))) + .andExpect(status().isForbidden()); + } + } + @Nested + @DisplayName("면접 질문들 가져오기") + class getQuestions { + @Test + @DisplayName("면접 질문을 저장하고 가져오기") + void getQuestions() throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(saveEmployerDto.getId(), "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + interviewRepository.saveQuestions(saveEmployeeDto.getId(), questionsDto); + + UserDetails employee = authService.loadUserByUsername(saveEmployeeDto.getEmail()); + MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/interview/questions/" + saveEmployerDto.getId() + "/" + saveEmployeeDto.getId()) + .with(user(employee))) + .andReturn(); + String responseBody = result.getResponse().getContentAsString(); + String companyNameOfEmployer = JsonPath.read(responseBody, "$.companyName"); + String question1OfEmployer = JsonPath.read(responseBody, "$.question1"); + String question2OfEmployer = JsonPath.read(responseBody, "$.question2"); + String question3OfEmployer = JsonPath.read(responseBody, "$.question3"); + assertThat(companyNameOfEmployer.equals(saveEmployerDto.getCompanyName())); + assertThat(question1OfEmployer.equals(questionsDto.getQuestion1())); + assertThat(question2OfEmployer.equals(questionsDto.getQuestion2())); + assertThat(question3OfEmployer.equals(questionsDto.getQuestion3())); + } + } + @Nested + @DisplayName("답변 제출") + class answerInterview{ + @Test + @DisplayName("면접을 시작 후 바로 나갔을 경우에 (1번문제 봄) status 확인") + void getQuestionsWhenDisconnected() throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(saveEmployerDto.getId(), "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + interviewRepository.saveQuestions(saveEmployeeDto.getId(),questionsDto); + UserDetails employee = authService.loadUserByUsername(saveEmployeeDto.getEmail()); + mvc.perform(put("/interview/start/" + saveEmployerDto.getId()) + .with(user(employee))); + assertThat(interviewRepository.getInterviewStatusOfUser(saveEmployerDto.getId(), saveEmployeeDto.getId())).isEqualTo(InterviewStatus.ING_Q1); + } + + @Test + @DisplayName("1번문제를 보는중 나가졌을 경우 1번문제의 답은 제출할 수 없다.") + void getQuestionsWhenDisconnectedInFirstQuestion() throws Exception { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(saveEmployerDto.getId(), "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + interviewRepository.saveQuestions(saveEmployeeDto.getId(),questionsDto); + UserDetails employee = authService.loadUserByUsername(saveEmployeeDto.getEmail()); + mvc.perform(put("/interview/start/" + saveEmployerDto.getId()) + .with(user(employee))); + + InterviewAnswerDto interviewAnswerDto = new InterviewAnswerDto(saveEmployerDto.getId(), 1,"1번문제의 답입니다."); + String firstAnswerString = objectMapper.writeValueAsString(interviewAnswerDto); + MvcResult result = mvc.perform(post("/interview/answers") + .with(user(employee)) + .contentType(MediaType.APPLICATION_JSON) + .content(firstAnswerString)) + .andExpect(status().isBadRequest()) + .andReturn(); + assertThat(result.getResolvedException().getMessage()).isEqualTo("잘못된 문제로 요청을 보내고 있습니다. 현 문제번호와 답변을 확인해주세요."); + } + } + +} \ No newline at end of file diff --git a/src/test/java/Funssion/Inforum/domain/interview/repository/InterviewRepositoryTest.java b/src/test/java/Funssion/Inforum/domain/interview/repository/InterviewRepositoryTest.java new file mode 100644 index 00000000..b68beba9 --- /dev/null +++ b/src/test/java/Funssion/Inforum/domain/interview/repository/InterviewRepositoryTest.java @@ -0,0 +1,74 @@ +package Funssion.Inforum.domain.interview.repository; + +import Funssion.Inforum.domain.interview.constant.InterviewStatus; +import Funssion.Inforum.domain.interview.dto.InterviewAnswerDto; +import Funssion.Inforum.domain.interview.dto.QuestionsDto; +import Funssion.Inforum.domain.interview.service.InterviewService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +class InterviewServiceTest { + @Autowired + InterviewService interviewService; + @Autowired + InterviewRepository interviewRepository; + + @Test + @DisplayName("답변 제출 후 문제 풀이 상태 갱신 확인") + void saveAnswerOfInterview(){ + Long employerId = 1L; + Long employeeId = 2L; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(employerId, "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + + interviewRepository.saveQuestions(employeeId,new QuestionsDto("1번 질문","2번 질문","3번 질문")); + + InterviewAnswerDto interviewAnswerDto = new InterviewAnswerDto(employerId, 1, "1번 답변입니다."); + interviewService.startInterview(employerId,employeeId); + assertThat(interviewService.saveAnswerOfQuestion(interviewAnswerDto,employeeId)).isEqualTo(InterviewStatus.ING_Q2); + } + + @Test + @DisplayName("2번답변 제출 후 문제 풀이 상태 갱신 확인") + void saveAnswerOfInterviewAfterDoneQuestion2(){ + Long employerId = 1L; + Long employeeId = 2L; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(employerId, "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + + interviewRepository.saveQuestions(employeeId,new QuestionsDto("1번 질문","2번 질문","3번 질문")); + + interviewService.startInterview(employerId,employeeId); + interviewService.saveAnswerOfQuestion(new InterviewAnswerDto(employerId,1,"1번 답변"),employeeId); + assertThat( interviewService.saveAnswerOfQuestion(new InterviewAnswerDto(employerId, 2, "2번 답변입니다."),employeeId)).isEqualTo(InterviewStatus.ING_Q3); + } + + @Test + @DisplayName("현재 상태와 다른 문제의 답변을 제출한 경우") + void saveAnswerOfInterviewMismatchStatus(){ + Long employerId = 1L; + Long employeeId = 2L; + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(employerId, "a1234567!", List.of(new SimpleGrantedAuthority("ROLE_EMPLOYER"))) + ); + + interviewRepository.saveQuestions(employeeId,new QuestionsDto("1번 질문","2번 질문","3번 질문")); + + assertThatThrownBy(()->interviewService.saveAnswerOfQuestion(new InterviewAnswerDto(employerId,2,"2번 답변"),employeeId)).hasMessage("잘못된 문제로 요청을 보내고 있습니다. 현 문제번호와 답변을 확인해주세요."); + } +} \ No newline at end of file