Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
testImplementation 'org.testcontainers:mysql'
testImplementation 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.awaitility:awaitility:4.2.0'

// Etc
implementation 'org.hibernate.validator:hibernate-validator'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.solidconnection.common.BaseEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand All @@ -25,6 +26,9 @@ public class ChatRoom extends BaseEntity {

private boolean isGroup = false;

@Column(name = "mentoring_id", unique = true)
private Long mentoringId;

@OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL)
@BatchSize(size = 10)
private final List<ChatParticipant> chatParticipants = new ArrayList<>();
Expand All @@ -35,4 +39,9 @@ public class ChatRoom extends BaseEntity {
public ChatRoom(boolean isGroup) {
this.isGroup = isGroup;
}

public ChatRoom(Long mentoringId, boolean isGroup) {
this.mentoringId = mentoringId;
this.isGroup = isGroup;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ SELECT COUNT(cm) FROM ChatMessage cm
AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt)
""")
long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId);

boolean existsByMentoringId(long mentoringId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,17 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long

simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
}

@Transactional
public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) {
if (chatRoomRepository.existsByMentoringId(mentoringId)) {
return;
}

ChatRoom chatRoom = new ChatRoom(mentoringId, false);
chatRoom = chatRoomRepository.save(chatRoom);
ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom);
ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom);
chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.mentor.dto;

public record MentoringApprovedEvent(
long mentoringId,
long mentorId,
long menteeId
) {

public static MentoringApprovedEvent of(long mentoringId, long mentorId, long menteeId) {
return new MentoringApprovedEvent(mentoringId, mentorId, menteeId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
import com.example.solidconnection.mentor.domain.Mentoring;
import com.example.solidconnection.mentor.dto.MentoringApplyRequest;
import com.example.solidconnection.mentor.dto.MentoringApplyResponse;
import com.example.solidconnection.mentor.dto.MentoringApprovedEvent;
import com.example.solidconnection.mentor.dto.MentoringConfirmRequest;
import com.example.solidconnection.mentor.dto.MentoringConfirmResponse;
import com.example.solidconnection.mentor.repository.MentorRepository;
import com.example.solidconnection.mentor.repository.MentoringRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -25,6 +27,7 @@ public class MentoringCommandService {

private final MentoringRepository mentoringRepository;
private final MentorRepository mentorRepository;
private final ApplicationEventPublisher eventPublisher;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

이벤트 처리의 트랜잭션 격리 의도와 실제 동작 일치 확인(승인 트랜잭션 영향 차단)

요구사항은 “채팅방 생성 실패가 멘토링 승인에 영향 주지 않음”입니다. 아래를 점검해 주세요.
1) 리스너 동작 방식. ApplicationEventPublisher는 기본 동기 호출이며, 동기 리스너가 예외를 던지면 여기로 전파되어 승인 트랜잭션이 롤백될 수 있습니다. REQUIRES_NEW는 리스너 트랜잭션 분리엔 도움되지만, 예외 전파를 막아주진 않습니다.
2) 권장 패턴. MentoringEventHandler에 @TransactionalEventListener(phase = AFTER_COMMIT)로 후커밋 실행을 보장하고, 내부에서 @transactional(propagation = REQUIRES_NEW) + 예외 캐치/로깅으로 원 트랜잭션을 보호하세요. 비동기 분리를 원하면 @async를 추가하고 예외를 반드시 삼켜야 합니다.
3) 테스트와의 정합성. 현재 테스트가 승인 직후 곧바로 채팅방을 조회해 성공한다면, 리스너는 동기일 가능성이 높습니다(또는 동기-후커밋). 의도와 어긋날 수 있으니 리스너 설정을 확인해 주세요.

예시(핵심 아이디어):
- @TransactionalEventListener(phase = AFTER_COMMIT)
- @async (선택)
- try/catch로 예외 흡수 + 로깅
- ChatService는 @transactional(REQUIRES_NEW)

원하시면 MentoringEventHandler 예시 코드를 제안드리겠습니다.

Also applies to: 54-55


아래 스크립트를 실행해 이벤트 핸들러 설정을 검증해 주세요.

  1. 멘토링 이벤트 핸들러 클래스 존재 확인
  2. @eventlistener, @TransactionalEventListener, @async 어노테이션 유무 확인
  3. ChatService 호출 위치 확인

🏁 Script executed:

#!/bin/bash
rg "class MentoringEventHandler" -n src
rg "@EventListener" -n src
rg "@TransactionalEventListener" -n src
rg "@Async" -n src
rg "ChatService" -n src

Length of output: 1989


추가 검증을 위해 이벤트 핸들러 코드를 확인합니다.


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' src/main/java/com/example/solidconnection/mentor/service/MentoringEventHandler.java

Length of output: 1001


이벤트 핸들러 트랜잭션 격리 및 예외 처리 강화하기

  1. @TransactionalEventListener(phase = AFTER_COMMIT) 적용
    승인 트랜잭션 커밋 이후에만 이벤트가 실행되도록 보장해 주세요.
  2. 예외 캐치 및 로깅 추가
    chatService.createMentoringChatRoom 호출을 try-catch로 감싸 예외 발생 시 로깅하고 승인 로직에 영향을 주지 않도록 보호하세요.
  3. ChatService 새 트랜잭션 분리
    createMentoringChatRoom 메서드에 @transactional(propagation = REQUIRES_NEW)를 붙여 별도 트랜잭션에서 안전하게 처리되게 해 주세요.
  4. @async 유지(선택)
    비동기 분리가 필요하다면 @async를 살려두되, 핸들러 내부에서 예외가 완전히 소화되도록 확인해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java
at line 30, enhance the event handler by annotating it with
@TransactionalEventListener(phase = AFTER_COMMIT) to ensure it runs only after
the approval transaction commits. Wrap the call to
chatService.createMentoringChatRoom in a try-catch block to log exceptions
without affecting the approval logic. Modify createMentoringChatRoom in
ChatService to have @Transactional(propagation = REQUIRES_NEW) for separate
transaction handling. If @Async is used, keep it but ensure all exceptions
inside the handler are fully caught and handled.


@Transactional
public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) {
Expand All @@ -48,6 +51,7 @@ public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoring

if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) {
mentor.increaseMenteeCount();
eventPublisher.publishEvent(MentoringApprovedEvent.of(mentoringId, mentor.getSiteUserId(), mentoring.getMenteeId()));
}

return MentoringConfirmResponse.from(mentoring);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.solidconnection.mentor.service;

import com.example.solidconnection.chat.service.ChatService;
import com.example.solidconnection.mentor.dto.MentoringApprovedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class MentoringEventHandler {

private final ChatService chatService;

@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener
public void handleMentoringApproved(MentoringApprovedEvent event) {
chatService.createMentoringChatRoom(event.mentoringId(), event.mentorId(), event.menteeId());
}
Comment on lines +18 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

3) 멱등성/경쟁 상태 대비 여부 확인 부탁드립니다.

  • createMentoringChatRoom 구현이 “존재하면 패스, 없으면 생성” 형태라면, 다중 이벤트/재시도/동시성에서 DB UQ 충돌로 DataIntegrityViolationException이 날 수 있어요.
  • 최종 방어선은 DB UQ가 맞지만, 서비스 레벨에서 UQ 충돌은 정상 시나리오로 취급(로그 레벨 낮춤·무시)하는 처리가 있으면 운영 소음이 줄어듭니다.
  • ChatService 쪽에서 UQ 위반을 캐치해 멱등 처리하는지 한번만 확인해 주세요.

아래 스크립트로 구현 확인 가능해요(예외 처리 유무, existsByMentoringId 사용 여부):


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "[Search] createMentoringChatRoom 구현"
rg -n --no-heading "createMentoringChatRoom\\s*\\(" -A 40
echo
echo "[Search] 멘토링 채팅 UQ/예외 처리 흔적"
rg -n --no-heading "DataIntegrityViolationException|ConstraintViolationException|existsByMentoringId"

Length of output: 4695


멱등성·경쟁 상태 보강 요청

  1. 멱등성 체크 현황
    • ChatService.createMentoringChatRoom 메소드에서 chatRoomRepository.existsByMentoringId(mentoringId) 로 이미 생성된 채팅방은 재생성하지 않습니다.
  2. 잠재적 경쟁 조건
    • 동시 다발적인 이벤트 처리 시 두 스레드가 거의 동시에 existsBy 검사를 통과하고 save 단계에서 DB의 고유 제약(UQ)을 위반해 DataIntegrityViolationException이 발생할 수 있습니다.
  3. 서비스 레벨 방어선 추가
    • createMentoringChatRoom 내부에서 chatRoomRepository.save(...)chatParticipantRepository.saveAll(...) 호출을
      try {
          // 기존 save 로직
      } catch (DataIntegrityViolationException ex) {
          log.debug("중복 채팅방 생성 시도 무시: mentoringId={}", mentoringId, ex);
      }
      형태로 감싸고, UQ 위반 시 DEBUG 레벨로 로깅 후 무시 처리해주세요.
    • 참고로 CustomExceptionHandler는 주로 컨트롤러 레이어의 예외를 처리하므로, 비동기 이벤트 핸들러 내 예외는 이 핸들러에서 잡히지 않습니다.
🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/mentor/service/MentoringEventHandler.java
around lines 18 to 23, add service-level defense against race conditions by
catching DataIntegrityViolationException around the chatRoomRepository.save(...)
and chatParticipantRepository.saveAll(...) calls invoked by
ChatService.createMentoringChatRoom: wrap the existing save logic in a try/catch
that catches DataIntegrityViolationException, logs a DEBUG message like "중복 채팅방
생성 시도 무시: mentoringId={}" with the mentoringId and exception, and then silently
return/ignore the exception so concurrent duplicate-creation attempts do not
crash the async event handler.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE chat_room
ADD COLUMN mentoring_id BIGINT,
ADD CONSTRAINT uk_chat_room_mentoring_id UNIQUE (mentoring_id),
ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
Comment on lines +1 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

1) 운영 락/가용성: 온라인 DDL 전략으로 분리해 주세요.

  • 한 문장으로 ALTER TABLE에 컬럼/UNIQUE/FK를 몰아 넣으면 대용량에서 쓰기 락이 길어질 수 있어요.

  • 배포 안전성을 위해 벤더별 온라인 DDL로 나누는 걸 권장해요.

    1. PostgreSQL 권장 예시
      • 부분 유니크 인덱스 + CONCURRENTLY + FK NOT VALID → VALIDATE 순서로 록 최소화.
      • 이 경우 해당 마이그레이션은 트랜잭션 밖에서 실행되어야 해요(executeInTransaction=false 분리, 또는 별도 마이그레이션 분할).
-ALTER TABLE chat_room
-    ADD COLUMN mentoring_id BIGINT,
-    ADD CONSTRAINT uk_chat_room_mentoring_id UNIQUE (mentoring_id),
-    ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
+-- 1) 컬럼 추가
+ALTER TABLE chat_room ADD COLUMN mentoring_id BIGINT;
+
+-- 2) NULL 제외 부분 유니크 인덱스(온라인)
+CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS uk_chat_room_mentoring_id_notnull
+  ON chat_room (mentoring_id) WHERE mentoring_id IS NOT NULL;
+
+-- 3) FK는 즉시 검증하지 않고 등록(삭제 정책은 SET NULL 예시)
+ALTER TABLE chat_room
+  ADD CONSTRAINT fk_chat_room_mentoring_id
+  FOREIGN KEY (mentoring_id) REFERENCES mentoring(id) ON DELETE SET NULL
+  NOT VALID;
+
+-- 4) 트래픽 한가한 시점에 검증
+ALTER TABLE chat_room VALIDATE CONSTRAINT fk_chat_room_mentoring_id;
2. MySQL(InnoDB) 권장 예시
	- 인덱스/컬럼 추가는 ALGORITHM=INPLACE, LOCK=NONE(가능 범위) 고려.  
	- FK 추가는 내부적으로 테이블 잠금이 필요할 수 있어, 트래픽 한가한 시간대 분리 적용 또는 pt-online-schema-change 고려.
-ALTER TABLE chat_room
-    ADD COLUMN mentoring_id BIGINT,
-    ADD CONSTRAINT uk_chat_room_mentoring_id UNIQUE (mentoring_id),
-    ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
+ALTER TABLE chat_room
+  ADD COLUMN mentoring_id BIGINT NULL,
+  ALGORITHM=INPLACE, LOCK=NONE;
+
+CREATE UNIQUE INDEX uk_chat_room_mentoring_id ON chat_room (mentoring_id)
+  ALGORITHM=INPLACE, LOCK=NONE;
+
+ALTER TABLE chat_room
+  ADD CONSTRAINT fk_chat_room_mentoring_id
+  FOREIGN KEY (mentoring_id) REFERENCES mentoring(id) ON DELETE SET NULL;
  • 필요하시면 Flyway 마이그레이션을 벤더별로 분기(프로필/환경)하거나, 컨커런트 인덱스가 필요한 경우 해당 스텝만 별도(비트랜잭션) 마이그레이션으로 분리하도록 초안 드릴게요.

운영 가용성 최적화를 위한 온라인 DDL 단계 분리 제안

  1. PostgreSQL 온라인 DDL 단계 분리

    • mentoring_id 컬럼을 ALTER TABLE로 먼저 추가해주세요.
    • CONCURRENTLY 옵션으로 NULL 제외 부분 유니크 인덱스를 생성해주세요.
    • NOT VALID 옵션으로 ON DELETE SET NULL 정책의 외래키 제약을 추가해주세요.
    • 이후 트래픽 여유 시점에 VALIDATE CONSTRAINT로 제약을 검증해주세요.
  2. MySQL InnoDB 온라인 DDL 단계 분리

    • ALGORITHM=INPLACE, LOCK=NONE 옵션으로 mentoring_id 컬럼을 추가해주세요.
    • 동일한 옵션으로 유니크 인덱스를 생성해주세요.
    • ON DELETE SET NULL 정책으로 외래키 제약을 추가하고, 필요 시 트래픽 여유 시점에 검증을 고려해주세요.
  3. Flyway 마이그레이션 분리

    • 벤더별 또는 비트랜잭션 단위로 마이그레이션을 분리하여 executeInTransaction=false 설정을 적용해주세요.
🤖 Prompt for AI Agents
In src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql around
lines 1 to 4, the current migration adds the mentoring_id column, unique
constraint and foreign key in a single blocking ALTER which risks downtime;
change to vendor-safe, staged DDL: for PostgreSQL, first ALTER TABLE ADD COLUMN
mentoring_id BIGINT; then create the unique index on mentoring_id WHERE
mentoring_id IS NOT NULL USING CONCURRENTLY; then add the FK with NOT VALID and
ON DELETE SET NULL, and validate it later with ALTER TABLE VALIDATE CONSTRAINT
when traffic allows; for MySQL/InnoDB, perform ALTER TABLE ... ADD COLUMN
mentoring_id BIGINT ALGORITHM=INPLACE, LOCK=NONE, then CREATE UNIQUE INDEX with
ALGORITHM=INPLACE, LOCK=NONE, then add the FK with ON DELETE SET NULL (consider
validating or checking afterward); finally split Flyway migrations by vendor or
into non-transactional steps and set executeInTransaction=false for the
non-transactional/CONCURRENTLY steps so they run safely outside a transaction.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

3) FK 삭제 정책 명시를 권장(도메인 의도에 맞게).

  • 현재는 기본 RESTRICT/NO ACTION로, mentoring 삭제 시 chat_room 삭제/유지 정책이 불명확해요.
  • 칼럼이 NULL 허용이므로, “멘토링 삭제 시 방은 남기되 연결만 끊기”라면 ON DELETE SET NULL이 자연스럽습니다.
  • “멘토링과 생명주기를 같이 한다”면 ON DELETE CASCADE가 맞고요.

예시(SET NULL):

-ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
+ADD CONSTRAINT fk_chat_room_mentoring_id
+  FOREIGN KEY (mentoring_id) REFERENCES mentoring(id) ON DELETE SET NULL;

외래키 삭제 정책 명시 요청

점검 위치:

  • 파일: src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql
  • 라인: 4

워크스루:

  1. 삭제 정책 추가
    기본 RESTRICT/NO ACTION으로 mentoring 삭제 시 chat_room 처리 방식이 불명확합니다.
  2. 도메인 의도에 맞는 정책 선택
    • 채팅방은 그대로 유지하고 연결만 끊으려면 ON DELETE SET NULL을 사용하세요.
    • mentoring과 생명주기를 함께 관리하려면 ON DELETE CASCADE를 사용하세요.
  3. 예시 diff
-ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
+ADD CONSTRAINT fk_chat_room_mentoring_id
+  FOREIGN KEY (mentoring_id) REFERENCES mentoring(id) ON DELETE SET NULL;

위 제안 중 도메인 요구사항에 맞는 정책으로 수정 부탁드립니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ADD CONSTRAINT fk_chat_room_mentoring_id FOREIGN KEY (mentoring_id) REFERENCES mentoring(id);
ADD CONSTRAINT fk_chat_room_mentoring_id
FOREIGN KEY (mentoring_id) REFERENCES mentoring(id) ON DELETE SET NULL;
🤖 Prompt for AI Agents
In src/main/resources/db/migration/V28__add_mentoring_id_to_chat_room.sql around
line 4, the FOREIGN KEY constraint is added without an explicit ON DELETE policy
which leaves behavior ambiguous; update the constraint to include a
domain-appropriate deletion policy (either ON DELETE SET NULL if chat_room
should remain and just unlink mentoring, or ON DELETE CASCADE if chat_room
should be removed when mentoring is removed), and if you choose SET NULL ensure
the mentoring_id column is nullable (alter column definition in this migration
if necessary); replace the current ADD CONSTRAINT line with the chosen FOREIGN
KEY clause including ON DELETE and apply corresponding column nullability
changes.


Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.solidconnection.chat.repository;

import com.example.solidconnection.chat.domain.ChatRoom;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ChatRoomRepositoryForTest extends JpaRepository<ChatRoom, Long> {

@Query("""
SELECT DISTINCT cr FROM ChatRoom cr
LEFT JOIN FETCH cr.chatParticipants cp
WHERE cr.isGroup = false
AND EXISTS (
SELECT 1 FROM ChatParticipant cp1
WHERE cp1.chatRoom = cr AND cp1.siteUserId = :mentorId
)
AND EXISTS (
SELECT 1 FROM ChatParticipant cp2
WHERE cp2.chatRoom = cr AND cp2.siteUserId = :menteeId
)
AND (
SELECT COUNT(cp3) FROM ChatParticipant cp3
WHERE cp3.chatRoom = cr
) = 2
""")
Optional<ChatRoom> findOneOnOneChatRoomByParticipants(@Param("mentorId") long mentorId, @Param("menteeId") long menteeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertAll;

import static org.awaitility.Awaitility.await;
import com.example.solidconnection.chat.domain.ChatParticipant;
import com.example.solidconnection.chat.domain.ChatRoom;
import com.example.solidconnection.chat.repository.ChatRoomRepositoryForTest;
import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.mentor.domain.Mentor;
Expand All @@ -22,6 +25,9 @@
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
import com.example.solidconnection.support.TestContainerSpringBootTest;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand All @@ -41,6 +47,9 @@ class MentoringCommandServiceTest {
@Autowired
private MentoringRepository mentoringRepository;

@Autowired
private ChatRoomRepositoryForTest chatRoomRepositoryForTest;

@Autowired
private SiteUserFixture siteUserFixture;

Expand Down Expand Up @@ -115,6 +124,36 @@ class 멘토링_승인_거절_테스트 {
);
}

@Test
void 멘토링_승인시_채팅방이_자동으로_생성된다() {
// given
Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId());
MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED);

Optional<ChatRoom> beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
assertThat(beforeChatRoom).isEmpty();

// when
mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request);

// then
ChatRoom afterChatRoom = await()
.atMost(Duration.ofSeconds(5))
.pollInterval(Duration.ofMillis(100))
.until(() -> chatRoomRepositoryForTest
.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()),
Optional::isPresent)
.orElseThrow();

List<Long> participantIds = afterChatRoom.getChatParticipants().stream()
.map(ChatParticipant::getSiteUserId)
.toList();
assertAll(
() -> assertThat(afterChatRoom.isGroup()).isFalse(),
() -> assertThat(participantIds).containsExactly(mentorUser1.getId(), menteeUser.getId())
);
}

@Test
void 멘토링을_성공적으로_거절한다() {
// given
Expand All @@ -137,6 +176,31 @@ class 멘토링_승인_거절_테스트 {
);
}

@Test
void 멘토링_거절시_채팅방이_자동으로_생성되지_않는다() {
// given
Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId());
MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED);

Optional<ChatRoom> beforeChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
assertThat(beforeChatRoom).isEmpty();

// when
mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request);

// then
await()
.pollInterval(Duration.ofMillis(100))
.during(Duration.ofSeconds(1))
.until(() -> chatRoomRepositoryForTest
.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId())
.isEmpty());

Optional<ChatRoom> afterChatRoom = chatRoomRepositoryForTest.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId());
assertThat(afterChatRoom).isEmpty();

}

@Test
void 다른_멘토의_멘토링을_승인할_수_없다() {
// given
Expand Down
Loading