diff --git a/src/main/java/com/example/trouble_log/domain/interview/controller/InterviewSubmissionController.java b/src/main/java/com/example/trouble_log/domain/interview/controller/InterviewSubmissionController.java index 04013cb..055e114 100644 --- a/src/main/java/com/example/trouble_log/domain/interview/controller/InterviewSubmissionController.java +++ b/src/main/java/com/example/trouble_log/domain/interview/controller/InterviewSubmissionController.java @@ -21,19 +21,19 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/projects") -@Tag(name = "Interview Submission", description = "면접 답변 최종 제출 및 개인정보 감지 API") +@Tag(name = "Interview Submission", description = "면접 답변 최종 제출 API") public class InterviewSubmissionController { private final InterviewSubmissionService interviewSubmissionService; @Operation( summary = "면접 답변 최종 제출", - description = "프로젝트 세션에 생성된 면접 질문 3개에 대한 최종 답변을 저장합니다. 답변 내 개인정보가 감지되면 제출을 차단하고 감지 유형과 경고 메시지를 반환합니다." + description = "프로젝트 세션에 생성된 면접 질문 3개에 대한 최종 답변을 저장합니다. 개인정보와 민감정보 감지는 최초 소스코드 입력 단계에서 수행합니다." ) @ApiResponses({ @ApiResponse(responseCode = "200", description = "최종 제출 성공", content = @Content(schema = @Schema(implementation = InterviewSubmitResponse.class))), - @ApiResponse(responseCode = "400", description = "필수 값 누락, 질문 3개 미준비, 답변 개수 불일치, 개인정보 감지"), + @ApiResponse(responseCode = "400", description = "필수 값 누락, 질문 3개 미준비, 답변 개수 불일치"), @ApiResponse(responseCode = "403", description = "해당 프로젝트 세션에 접근할 수 없음"), @ApiResponse(responseCode = "404", description = "프로젝트 세션을 찾을 수 없음") }) diff --git a/src/main/java/com/example/trouble_log/domain/interview/dto/PersonalInfoDetectionResponse.java b/src/main/java/com/example/trouble_log/domain/interview/dto/PersonalInfoDetectionResponse.java new file mode 100644 index 0000000..92505d3 --- /dev/null +++ b/src/main/java/com/example/trouble_log/domain/interview/dto/PersonalInfoDetectionResponse.java @@ -0,0 +1,18 @@ +package com.example.trouble_log.domain.interview.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@Schema(description = "개인정보 감지 차단 응답") +public class PersonalInfoDetectionResponse { + + @Schema(description = "차단 사유", example = "PERSONAL_INFO_DETECTED") + private String blockedReason; + + @Schema(description = "개인정보 감지 경고 목록") + private List warnings; +} diff --git a/src/main/java/com/example/trouble_log/domain/interview/service/InterviewService.java b/src/main/java/com/example/trouble_log/domain/interview/service/InterviewService.java index b91eca2..dc1d513 100644 --- a/src/main/java/com/example/trouble_log/domain/interview/service/InterviewService.java +++ b/src/main/java/com/example/trouble_log/domain/interview/service/InterviewService.java @@ -34,6 +34,9 @@ @Transactional(readOnly = true) public class InterviewService { + private static final String DEFAULT_IMPROVEMENT_MESSAGE = + "현재 답변은 질문의 핵심을 잘 짚고 있어요. 이 흐름을 유지하면서 본인이 직접 고민하고 해결한 과정까지 차분히 이어가면 더 설득력 있는 답변이 될 수 있습니다."; + private final InterviewQuestionRepository questionRepository; private final InterviewAnswerRepository answerRepository; private final ProjectSessionRepository sessionRepository; @@ -93,7 +96,7 @@ public AnswerResponse saveAnswer(Long sessionId, Long questionId, AnswerRequest feedbackJson = "{}"; } - improvement = feedbackResult.getImprovement(); + improvement = normalizeImprovement(feedbackResult.getImprovement()); warning = feedbackResult.getWarning(); } @@ -109,6 +112,12 @@ public AnswerResponse saveAnswer(Long sessionId, Long questionId, AnswerRequest return new AnswerResponse(saved.getId(), improvement, warning); } + private String normalizeImprovement(String improvement) { + return improvement == null || improvement.isBlank() + ? DEFAULT_IMPROVEMENT_MESSAGE + : improvement; + } + // ── 트러블슈팅 리포트 생성 ──────────────────────────────── @Transactional public ReportResponse generateReport(Long sessionId) { diff --git a/src/main/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionService.java b/src/main/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionService.java index 8becaee..6f964e0 100644 --- a/src/main/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionService.java +++ b/src/main/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionService.java @@ -4,10 +4,8 @@ import com.example.trouble_log.domain.interview.dto.InterviewSubmitAnswerRequest; import com.example.trouble_log.domain.interview.dto.InterviewSubmitRequest; import com.example.trouble_log.domain.interview.dto.InterviewSubmitResponse; -import com.example.trouble_log.domain.interview.dto.PersonalInfoWarning; import com.example.trouble_log.domain.interview.entity.InterviewAnswer; import com.example.trouble_log.domain.interview.entity.InterviewQuestion; -import com.example.trouble_log.domain.interview.exception.PersonalInfoDetectedException; import com.example.trouble_log.domain.interview.repository.InterviewAnswerRepository; import com.example.trouble_log.domain.interview.repository.InterviewQuestionRepository; import com.example.trouble_log.domain.projectSession.entity.ProjectSession; @@ -17,7 +15,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -31,15 +28,13 @@ public class InterviewSubmissionService { private static final int REQUIRED_QUESTION_COUNT = 3; - private static final String UNANSWERED_MESSAGE = "사용자 답변 안 함"; private final ProjectSessionRepository projectSessionRepository; private final InterviewQuestionRepository interviewQuestionRepository; private final InterviewAnswerRepository interviewAnswerRepository; - private final PersonalInfoDetectionService personalInfoDetectionService; private final AuditLogService auditLogService; - // 최종 답변 3개를 저장한 뒤 개인정보 감지 결과에 따라 리포트 생성 가능 상태를 반환한다. + // 최종 답변 3개를 저장한 뒤 리포트 생성 가능 상태를 반환한다. @Transactional public InterviewSubmitResponse submit(Long sessionId, InterviewSubmitRequest request) { validateRequest(sessionId, request); @@ -55,19 +50,6 @@ public InterviewSubmitResponse submit(Long sessionId, InterviewSubmitRequest req Map finalAnswers = validateAndNormalizeFinalAnswers(questions, request.getAnswers()); List savedAnswers = saveFinalAnswers(questions, finalAnswers); - String answersText = buildAnswersText(questions, savedAnswers); - List warnings = personalInfoDetectionService.detect(answersText); - if (!warnings.isEmpty()) { - auditLogService.recordFailure( - request.getMemberId(), - sessionId, - "INTERVIEW_ANSWER_SUBMIT", - buildSubmitRequestSummary(questions, savedAnswers), - "개인정보 감지: " + warnings.stream().map(PersonalInfoWarning::getType).toList() - ); - throw new PersonalInfoDetectedException(warnings); - } - auditLogService.recordSuccess( request.getMemberId(), sessionId, @@ -176,21 +158,6 @@ private InterviewAnswer saveFinalAnswer(InterviewQuestion question, String final return interviewAnswerRepository.save(interviewAnswer); } - // 개인정보 감지와 리포트 생성 요청에 사용할 질문/답변 텍스트를 만든다. - private String buildAnswersText(List questions, List answers) { - Map answerByQuestionId = answers.stream() - .collect(Collectors.toMap(answer -> answer.getInterviewQuestion().getId(), Function.identity())); - - return questions.stream() - .map(question -> question.getQuestion() + "\n" + toReportAnswer(answerByQuestionId.get(question.getId()).getAnswer())) - .reduce("", (left, right) -> left + "\n" + right); - } - - // 답변이 비어 있으면 AI 리포트가 미답변 상태를 이해할 수 있는 명시 문구로 변환한다. - private String toReportAnswer(String answer) { - return answer == null || answer.isBlank() ? UNANSWERED_MESSAGE : answer; - } - // 최종 제출 요청 내용을 감사 로그용 요약 문자열로 변환한다. private String buildSubmitRequestSummary(List questions, List answers) { int answerLength = answers.stream() diff --git a/src/main/java/com/example/trouble_log/domain/projectSession/controller/ProjectSessionController.java b/src/main/java/com/example/trouble_log/domain/projectSession/controller/ProjectSessionController.java index 38ec7c4..cdc7952 100644 --- a/src/main/java/com/example/trouble_log/domain/projectSession/controller/ProjectSessionController.java +++ b/src/main/java/com/example/trouble_log/domain/projectSession/controller/ProjectSessionController.java @@ -1,5 +1,6 @@ package com.example.trouble_log.domain.projectSession.controller; +import com.example.trouble_log.domain.interview.dto.PersonalInfoDetectionResponse; import com.example.trouble_log.domain.projectSession.dto.PreContextRequest; import com.example.trouble_log.domain.projectSession.dto.PreContextResponse; import com.example.trouble_log.domain.projectSession.dto.ProjectSessionRequest; @@ -31,12 +32,13 @@ public class ProjectSessionController { @Operation( summary = "프로젝트 세션 생성", - description = "회원이 입력한 소스코드와 선택적인 GitHub URL을 저장하고, 이후 사전 컨텍스트와 면접 질문 생성에 사용할 세션 ID를 반환합니다." + description = "회원이 입력한 소스코드에서 개인정보와 민감정보를 먼저 감지합니다. 문제가 없으면 소스코드와 선택적인 GitHub URL을 저장하고, 이후 사전 컨텍스트와 면접 질문 생성에 사용할 세션 ID를 반환합니다." ) @ApiResponses({ @ApiResponse(responseCode = "201", description = "프로젝트 세션 생성 성공", content = @Content(schema = @Schema(implementation = ProjectSessionResponse.class))), - @ApiResponse(responseCode = "400", description = "회원 ID 또는 소스코드 누락"), + @ApiResponse(responseCode = "400", description = "회원 ID 또는 소스코드 누락, 개인정보 또는 민감정보 감지", + content = @Content(schema = @Schema(implementation = PersonalInfoDetectionResponse.class))), @ApiResponse(responseCode = "404", description = "회원을 찾을 수 없음") }) @PostMapping() diff --git a/src/main/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionService.java b/src/main/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionService.java index ec65aa8..5da8483 100644 --- a/src/main/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionService.java +++ b/src/main/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionService.java @@ -3,8 +3,11 @@ import com.example.trouble_log.domain.ai.service.AzureOpenAiPromptService; import com.example.trouble_log.domain.audit.service.AuditLogService; import com.example.trouble_log.domain.interview.dto.InterviewQuestionResponse; +import com.example.trouble_log.domain.interview.dto.PersonalInfoWarning; import com.example.trouble_log.domain.interview.entity.InterviewQuestion; +import com.example.trouble_log.domain.interview.exception.PersonalInfoDetectedException; import com.example.trouble_log.domain.interview.repository.InterviewQuestionRepository; +import com.example.trouble_log.domain.interview.service.PersonalInfoDetectionService; import com.example.trouble_log.domain.projectSession.dto.PreContextRequest; import com.example.trouble_log.domain.projectSession.dto.PreContextResponse; import com.example.trouble_log.domain.projectSession.dto.ProjectSessionRequest; @@ -37,12 +40,14 @@ public class ProjectSessionService { private final InterviewQuestionRepository interviewQuestionRepository; private final AzureOpenAiPromptService azureOpenAiPromptService; private final AuditLogService auditLogService; + private final PersonalInfoDetectionService personalInfoDetectionService; @Transactional // 회원의 코드와 GitHub URL을 기반으로 새 프로젝트 세션을 생성한다. public ProjectSessionResponse create(ProjectSessionRequest request) { validateRequestBody(request); validateRequiredFields("회원 ID와 소스코드는 필수입니다.", request.getMemberId(), request.getCodeContent()); + validateCodeContentDoesNotContainPersonalInfo(request); Member member = memberRepository.findById(request.getMemberId()) .orElseThrow(() -> { @@ -75,6 +80,22 @@ public ProjectSessionResponse create(ProjectSessionRequest request) { return new ProjectSessionResponse(savedProjectSession.getId()); } + private void validateCodeContentDoesNotContainPersonalInfo(ProjectSessionRequest request) { + List warnings = personalInfoDetectionService.detect(request.getCodeContent()); + if (warnings.isEmpty()) { + return; + } + + auditLogService.recordFailure( + request.getMemberId(), + null, + "PROJECT_SESSION_CREATE", + buildProjectSessionRequestSummary(request), + "개인정보 감지: " + warnings.stream().map(PersonalInfoWarning::getType).toList() + ); + throw new PersonalInfoDetectedException(warnings); + } + @Transactional // 사전 컨텍스트를 저장하고 AI 면접 질문을 생성해 함께 반환한다. public PreContextResponse createPreContext(Long sessionId, PreContextRequest request) { diff --git a/src/main/java/com/example/trouble_log/global/exception/GlobalExceptionHandler.java b/src/main/java/com/example/trouble_log/global/exception/GlobalExceptionHandler.java index aefc67c..a370f4f 100644 --- a/src/main/java/com/example/trouble_log/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/trouble_log/global/exception/GlobalExceptionHandler.java @@ -1,8 +1,7 @@ package com.example.trouble_log.global.exception; -import com.example.trouble_log.domain.interview.dto.InterviewSubmitResponse; +import com.example.trouble_log.domain.interview.dto.PersonalInfoDetectionResponse; import com.example.trouble_log.domain.interview.exception.PersonalInfoDetectedException; -import java.util.List; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -12,10 +11,8 @@ public class GlobalExceptionHandler { @ExceptionHandler(PersonalInfoDetectedException.class) - public ResponseEntity handlePersonalInfoDetected(PersonalInfoDetectedException e) { - InterviewSubmitResponse response = new InterviewSubmitResponse( - false, - false, + public ResponseEntity handlePersonalInfoDetected(PersonalInfoDetectedException e) { + PersonalInfoDetectionResponse response = new PersonalInfoDetectionResponse( "PERSONAL_INFO_DETECTED", e.getWarnings() ); diff --git a/src/test/java/com/example/trouble_log/domain/interview/service/InterviewServiceTest.java b/src/test/java/com/example/trouble_log/domain/interview/service/InterviewServiceTest.java index 57d924a..a3b6a72 100644 --- a/src/test/java/com/example/trouble_log/domain/interview/service/InterviewServiceTest.java +++ b/src/test/java/com/example/trouble_log/domain/interview/service/InterviewServiceTest.java @@ -30,6 +30,7 @@ class InterviewServiceTest { private InterviewAnswerRepository answerRepository; + private AzureOpenAiPromptService promptService; private InterviewService interviewService; @BeforeEach @@ -38,7 +39,7 @@ void setUp() { answerRepository = mock(InterviewAnswerRepository.class); ProjectSessionRepository sessionRepository = mock(ProjectSessionRepository.class); PreContextRepository preContextRepository = mock(PreContextRepository.class); - AzureOpenAiPromptService promptService = mock(AzureOpenAiPromptService.class); + promptService = mock(AzureOpenAiPromptService.class); interviewService = new InterviewService( questionRepository, @@ -85,6 +86,48 @@ void saveAnswerUpdatesExistingAnswerInsteadOfCreatingDuplicate() { assertThat(existingAnswer.getFeedback()).isNotBlank(); } + @Test + void saveAnswerReturnsDefaultImprovementWhenAiImprovementIsNull() { + AnswerFeedbackResult feedbackResult = new AnswerFeedbackResult(); + feedbackResult.setImprovement(null); + when(promptService.evaluateAnswer(any(ProjectSession.class), any(PreContext.class), any(), any())) + .thenReturn(feedbackResult); + + InterviewAnswer existingAnswer = new InterviewAnswer(question(), null, true, null); + ReflectionTestUtils.setField(existingAnswer, "id", 10L); + when(answerRepository.findByInterviewQuestionId(1L)).thenReturn(Optional.of(existingAnswer)); + + AnswerRequest request = new AnswerRequest(); + request.setAnswer("좋은 답변입니다."); + + AnswerResponse response = interviewService.saveAnswer(1L, 1L, request); + + assertThat(response.getImprovement()).isEqualTo( + "현재 답변은 질문의 핵심을 잘 짚고 있어요. 이 흐름을 유지하면서 본인이 직접 고민하고 해결한 과정까지 차분히 이어가면 더 설득력 있는 답변이 될 수 있습니다." + ); + } + + @Test + void saveAnswerReturnsDefaultImprovementWhenAiImprovementIsBlank() { + AnswerFeedbackResult feedbackResult = new AnswerFeedbackResult(); + feedbackResult.setImprovement(" "); + when(promptService.evaluateAnswer(any(ProjectSession.class), any(PreContext.class), any(), any())) + .thenReturn(feedbackResult); + + InterviewAnswer existingAnswer = new InterviewAnswer(question(), null, true, null); + ReflectionTestUtils.setField(existingAnswer, "id", 10L); + when(answerRepository.findByInterviewQuestionId(1L)).thenReturn(Optional.of(existingAnswer)); + + AnswerRequest request = new AnswerRequest(); + request.setAnswer("좋은 답변입니다."); + + AnswerResponse response = interviewService.saveAnswer(1L, 1L, request); + + assertThat(response.getImprovement()).isEqualTo( + "현재 답변은 질문의 핵심을 잘 짚고 있어요. 이 흐름을 유지하면서 본인이 직접 고민하고 해결한 과정까지 차분히 이어가면 더 설득력 있는 답변이 될 수 있습니다." + ); + } + private ProjectSession projectSession() { Member member = new Member("user@example.com", "password", "user"); ReflectionTestUtils.setField(member, "id", 1L); diff --git a/src/test/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionServiceTest.java b/src/test/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionServiceTest.java index ecfe5f7..43770fc 100644 --- a/src/test/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionServiceTest.java +++ b/src/test/java/com/example/trouble_log/domain/interview/service/InterviewSubmissionServiceTest.java @@ -1,7 +1,6 @@ package com.example.trouble_log.domain.interview.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; @@ -13,7 +12,6 @@ import com.example.trouble_log.domain.interview.dto.InterviewSubmitResponse; import com.example.trouble_log.domain.interview.entity.InterviewAnswer; import com.example.trouble_log.domain.interview.entity.InterviewQuestion; -import com.example.trouble_log.domain.interview.exception.PersonalInfoDetectedException; import com.example.trouble_log.domain.interview.repository.InterviewAnswerRepository; import com.example.trouble_log.domain.interview.repository.InterviewQuestionRepository; import com.example.trouble_log.domain.projectSession.entity.ProjectSession; @@ -41,7 +39,6 @@ void setUp() { projectSessionRepository, interviewQuestionRepository, interviewAnswerRepository, - new PersonalInfoDetectionService(), mock(AuditLogService.class) ); @@ -50,7 +47,7 @@ void setUp() { } @Test - void submitBlocksWhenAnswerContainsPersonalInfo() { + void submitDoesNotBlockWhenAnswerContainsPersonalInfo() { ProjectSession projectSession = projectSession(); InterviewSubmitRequest request = request(List.of( answerRequest(1L, "제 이메일은 test@example.com 입니다."), @@ -67,11 +64,10 @@ void submitBlocksWhenAnswerContainsPersonalInfo() { when(interviewQuestionRepository.findByProjectSessionIdOrderByQuestionSequenceAsc(anyLong())) .thenReturn(questions); - assertThatThrownBy(() -> interviewSubmissionService.submit(1L, request)) - .isInstanceOf(PersonalInfoDetectedException.class) - .satisfies(e -> assertThat(((PersonalInfoDetectedException) e).getWarnings()) - .extracting("type") - .containsExactly(com.example.trouble_log.domain.interview.type.PersonalInfoType.EMAIL)); + InterviewSubmitResponse response = interviewSubmissionService.submit(1L, request); + + assertThat(response.getSubmitted()).isTrue(); + assertThat(response.getReportGenerationReady()).isTrue(); } @Test diff --git a/src/test/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionServiceTest.java b/src/test/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionServiceTest.java new file mode 100644 index 0000000..19aa691 --- /dev/null +++ b/src/test/java/com/example/trouble_log/domain/projectSession/service/ProjectSessionServiceTest.java @@ -0,0 +1,89 @@ +package com.example.trouble_log.domain.projectSession.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.trouble_log.domain.ai.service.AzureOpenAiPromptService; +import com.example.trouble_log.domain.audit.service.AuditLogService; +import com.example.trouble_log.domain.interview.exception.PersonalInfoDetectedException; +import com.example.trouble_log.domain.interview.repository.InterviewQuestionRepository; +import com.example.trouble_log.domain.interview.service.PersonalInfoDetectionService; +import com.example.trouble_log.domain.projectSession.dto.ProjectSessionRequest; +import com.example.trouble_log.domain.projectSession.dto.ProjectSessionResponse; +import com.example.trouble_log.domain.projectSession.entity.ProjectSession; +import com.example.trouble_log.domain.projectSession.repository.PreContextRepository; +import com.example.trouble_log.domain.projectSession.repository.ProjectSessionRepository; +import com.example.trouble_log.domain.user.entity.Member; +import com.example.trouble_log.domain.user.repository.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +class ProjectSessionServiceTest { + + private MemberRepository memberRepository; + private ProjectSessionRepository projectSessionRepository; + private ProjectSessionService projectSessionService; + + @BeforeEach + void setUp() { + memberRepository = mock(MemberRepository.class); + projectSessionRepository = mock(ProjectSessionRepository.class); + projectSessionService = new ProjectSessionService( + memberRepository, + projectSessionRepository, + mock(PreContextRepository.class), + mock(InterviewQuestionRepository.class), + mock(AzureOpenAiPromptService.class), + mock(AuditLogService.class), + new PersonalInfoDetectionService() + ); + } + + @Test + void createBlocksWhenCodeContentContainsPersonalInfo() { + ProjectSessionRequest request = new ProjectSessionRequest( + 1L, + "const email = \"test@example.com\";", + null + ); + + assertThatThrownBy(() -> projectSessionService.create(request)) + .isInstanceOf(PersonalInfoDetectedException.class); + + verify(projectSessionRepository, never()).save(any(ProjectSession.class)); + } + + @Test + void createSavesProjectSessionWhenCodeContentDoesNotContainPersonalInfo() { + Member member = new Member("user@example.com", "password", "user"); + ReflectionTestUtils.setField(member, "id", 1L); + + ProjectSession savedProjectSession = new ProjectSession( + member, + "public class Main { public static void main(String[] args) { } }", + "https://github.com/user/project" + ); + ReflectionTestUtils.setField(savedProjectSession, "id", 10L); + + ProjectSessionRequest request = new ProjectSessionRequest( + 1L, + "public class Main { public static void main(String[] args) { } }", + "https://github.com/user/project" + ); + + when(memberRepository.findById(1L)).thenReturn(Optional.of(member)); + when(projectSessionRepository.save(any(ProjectSession.class))).thenReturn(savedProjectSession); + + ProjectSessionResponse response = projectSessionService.create(request); + + assertThat(response.getSessionId()).isEqualTo(10L); + verify(projectSessionRepository).save(any(ProjectSession.class)); + } +}