diff --git a/build.gradle b/build.gradle index 23488ef..2b46879 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,12 @@ plugins { id 'checkstyle' } +configurations { + all { + exclude group: 'commons-logging', module: 'commons-logging' + } +} + editorconfig { excludes = ['build'] } diff --git a/config/naver-checkstyle-rules.xml b/config/naver-checkstyle-rules.xml index 1d93b2f..06d3541 100644 --- a/config/naver-checkstyle-rules.xml +++ b/config/naver-checkstyle-rules.xml @@ -221,11 +221,11 @@ The following rules in the Naver coding convention cannot be checked by this con value="[sub-flow-after-brace] ''{0}'' at column {1} should have line break before."/> - - - - + + + + + diff --git a/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java b/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java index 022c337..84be3de 100644 --- a/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java +++ b/src/main/java/capstone/relation/api/auth/jwt/SecurityUser.java @@ -72,7 +72,7 @@ public Long getUserId() { return userId; } - private SecurityUser(Long userId, Role role) { + public SecurityUser(Long userId, Role role) { this.userId = userId; this.role = role; } diff --git a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java index 7b62a81..411fb64 100644 --- a/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java +++ b/src/main/java/capstone/relation/api/auth/jwt/TokenProvider.java @@ -122,8 +122,7 @@ public String getWorkSpaceIdByInviteCode(String inviteCode) { } } - private String generateAccessToken(User user, Date accessTokenExpiredDate) { - + public String generateAccessToken(User user, Date accessTokenExpiredDate) { return Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim(jwtProperties.getAuthorityKey(), user.getRole().toString()) diff --git a/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java b/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java index cdcbe7c..e0cb62f 100644 --- a/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java +++ b/src/main/java/capstone/relation/common/error/GlobalExceptionHandler.java @@ -18,6 +18,17 @@ public ResponseEntity handleResponseStatusException(ResponseStatusExcept return new ResponseEntity<>(ex.getReason(), ex.getStatusCode()); } + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex, + HttpServletRequest request) { + ProblemDetail problemDetail = ProblemDetailCreator.create(ex, request, HttpStatus.BAD_REQUEST); + problemDetail.setTitle("Invalid Argument"); + problemDetail.setStatus(HttpStatus.BAD_REQUEST.value()); + problemDetail.setDetail(ex.getMessage()); + + return ResponseEntity.badRequest().body(problemDetail); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleAllException(Exception ex, HttpServletRequest request) { ProblemDetail problemDetail = ProblemDetailCreator.create(ex, request, HttpStatus.BAD_REQUEST); diff --git a/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java b/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java index 3bbdaee..57cca16 100644 --- a/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java +++ b/src/main/java/capstone/relation/common/initializer/CustomInitEvent.java @@ -1,7 +1,9 @@ package capstone.relation.common.initializer; import org.springframework.context.ApplicationEvent; +import org.springframework.context.annotation.Profile; +@Profile("dev") public class CustomInitEvent extends ApplicationEvent { // 여기에 추가 데이터 필드나 메서드를 정의할 수 있습니다. diff --git a/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java b/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java index 501226a..e21e504 100644 --- a/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java +++ b/src/main/java/capstone/relation/common/initializer/DatabaseInitializer.java @@ -2,6 +2,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Profile; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; @@ -9,6 +10,7 @@ @Component @RequiredArgsConstructor +@Profile("dev") public class DatabaseInitializer implements ApplicationListener { private final ApplicationEventPublisher eventPublisher; diff --git a/src/main/java/capstone/relation/common/util/SecurityUtil.java b/src/main/java/capstone/relation/common/util/SecurityUtil.java new file mode 100644 index 0000000..3ff8392 --- /dev/null +++ b/src/main/java/capstone/relation/common/util/SecurityUtil.java @@ -0,0 +1,30 @@ +package capstone.relation.common.util; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import capstone.relation.api.auth.jwt.SecurityUser; + +public class SecurityUtil { + + private SecurityUtil() { + throw new IllegalStateException("Utility class"); + } + + public static UserDetails getCurrentUserDetails() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.getPrincipal() instanceof UserDetails) { + return (UserDetails)authentication.getPrincipal(); + } + throw new IllegalStateException("User not authenticated"); + } + + public static Long getCurrentUserId() { + UserDetails userDetails = getCurrentUserDetails(); + if (userDetails instanceof SecurityUser) { // Assuming SecurityUser extends UserDetails + return ((SecurityUser)userDetails).getUserId(); + } + throw new IllegalStateException("UserDetails does not contain userId"); + } +} diff --git a/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java b/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java index fc1d7c3..f1e3dba 100644 --- a/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java +++ b/src/main/java/capstone/relation/meeting/controller/MeetRoomController.java @@ -1,20 +1,17 @@ package capstone.relation.meeting.controller; -import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.server.ResponseStatusException; +import capstone.relation.common.util.SecurityUtil; import capstone.relation.meeting.dto.request.CreateRoomDto; import capstone.relation.meeting.dto.response.JoinResponseDto; import capstone.relation.meeting.dto.response.MeetingRoomListDto; import capstone.relation.meeting.service.MeetRoomService; -import capstone.relation.user.UserService; -import capstone.relation.user.domain.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -25,31 +22,20 @@ @RequiredArgsConstructor public class MeetRoomController { private final MeetRoomService meetRoomService; - private final UserService userService; @PostMapping("/create") @Operation(summary = "회의방 생성", description = "새로운 회의방을 생성합니다. 생성된 회의방은 `/topic/{workSpaceId}/meetingRoomList`로 생성된 방에 대한 목록 발송이 이루어집니다.\n" + "방 생성자는 자동으로 방에 참여합니다.\n" ) - public JoinResponseDto createRoom(@RequestBody CreateRoomDto createRoomDto) throws Exception { - User user = userService.getUserEntity(); - try { - return meetRoomService.createAndJoinRoom(createRoomDto, user.getId(), user.getWorkSpace().getId()); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); - } + public JoinResponseDto createRoom(@RequestBody CreateRoomDto createRoomDto) { + return meetRoomService.createAndJoinRoom(createRoomDto); } @PostMapping("/join/{roomId}") @Operation(summary = "회의방 참여", description = "회의방에 참여합니다.") public JoinResponseDto joinRoom(@PathVariable Long roomId) { - User user = userService.getUserEntity(); - try { - return meetRoomService.joinRoom(user.getId(), user.getWorkSpace().getId(), roomId); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); - } + return meetRoomService.joinRoom(roomId); } @GetMapping("/list") @@ -57,14 +43,12 @@ public JoinResponseDto joinRoom(@PathVariable Long roomId) { + "이것에 대한 응답은 `/topic/{workSpaceId}/meetingRoomList`로 이루어집니다.\n" ) public MeetingRoomListDto requestRoomList() { - User user = userService.getUserEntity(); - return meetRoomService.sendRoomList(user.getWorkSpace().getId()); + return meetRoomService.sendRoomList(); } @PostMapping("/leave") @Operation(summary = "회의방 나가기", description = "현재 참여중인 회의방을 나갑니다.") public void leaveRoom() { - User user = userService.getUserEntity(); - meetRoomService.leaveRoom(user.getId(), user.getWorkSpace().getId()); + meetRoomService.leaveRoom(SecurityUtil.getCurrentUserId()); } } diff --git a/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java b/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java index 0c78c71..a83523b 100644 --- a/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java +++ b/src/main/java/capstone/relation/meeting/dto/response/JoinResponseDto.java @@ -5,10 +5,12 @@ import capstone.relation.user.dto.UserInfoDto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; @Data @AllArgsConstructor +@Builder public class JoinResponseDto { @Schema(description = "회의실 ID", example = "123") private Long roomId; diff --git a/src/main/java/capstone/relation/meeting/repository/RedisRepository.java b/src/main/java/capstone/relation/meeting/repository/RedisRepository.java new file mode 100644 index 0000000..6df6288 --- /dev/null +++ b/src/main/java/capstone/relation/meeting/repository/RedisRepository.java @@ -0,0 +1,83 @@ +package capstone.relation.meeting.repository; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class RedisRepository { + private static final String WORK_KEY = "WORKSPACE_ROOM_PARTICIPANTS"; + private static final String USER_KEY = "USER_ROOM_MAPPING"; + private final RedisTemplate redisTemplate; + private HashOperations>> workspaceRoomParticipants; + private HashOperations userRoomMapping; + + @PostConstruct + protected void init() { + workspaceRoomParticipants = redisTemplate.opsForHash(); + userRoomMapping = redisTemplate.opsForHash(); + } + + public boolean isUserInRoom(Long userId) { + return userRoomMapping.get(USER_KEY, userId.toString()) != null; + } + + public String getUserRoomId(Long userId) { + return userRoomMapping.get(USER_KEY, userId.toString()); + } + + public void addUserToRoom(String workspaceId, Long roomId, String userId) { + HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId); + if (roomParticipants == null) + roomParticipants = new HashMap<>(); + Set users = roomParticipants.get(roomId.toString()); + if (users == null) + users = new HashSet<>(); + + users.add(userId); + roomParticipants.put(roomId.toString(), users); + workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants); + userRoomMapping.put(USER_KEY, userId, roomId.toString()); + } + + public void removeUserFromRoom(String workspaceId, Long roomId, String userId) { + userRoomMapping.delete(USER_KEY, userId); + HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId); + if (roomParticipants == null) + return; + Set userIds = roomParticipants.get(roomId.toString()); + userIds.remove(userId); + + if (userIds.isEmpty()) + roomParticipants.remove(roomId.toString()); + else + roomParticipants.put(roomId.toString(), userIds); + + if (roomParticipants.isEmpty()) + workspaceRoomParticipants.delete(WORK_KEY, workspaceId); + else + workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants); + + } + + public Set getRoomMemberIds(String workspaceId, Long roomId) { + HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId); + if (roomParticipants == null) { + return new HashSet<>(); + } + return roomParticipants.get(roomId.toString()); + } + + public void deleteAll() { + redisTemplate.delete(WORK_KEY); + redisTemplate.delete(USER_KEY); + } +} diff --git a/src/main/java/capstone/relation/meeting/service/MeetRoomService.java b/src/main/java/capstone/relation/meeting/service/MeetRoomService.java index 6dcff03..c609d84 100644 --- a/src/main/java/capstone/relation/meeting/service/MeetRoomService.java +++ b/src/main/java/capstone/relation/meeting/service/MeetRoomService.java @@ -1,244 +1,206 @@ package capstone.relation.meeting.service; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Set; -import org.springframework.data.redis.core.HashOperations; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; -import capstone.relation.api.auth.exception.AuthException; +import capstone.relation.common.util.SecurityUtil; import capstone.relation.meeting.domain.MeetRoom; import capstone.relation.meeting.dto.request.CreateRoomDto; import capstone.relation.meeting.dto.response.JoinResponseDto; import capstone.relation.meeting.dto.response.MeetingRoomDto; import capstone.relation.meeting.dto.response.MeetingRoomListDto; import capstone.relation.meeting.repository.MeetRoomRepository; +import capstone.relation.meeting.repository.RedisRepository; +import capstone.relation.user.UserService; import capstone.relation.user.dto.RoomInfoDto; import capstone.relation.user.dto.UserInfoDto; -import capstone.relation.user.repository.UserRepository; -import capstone.relation.websocket.SocketRegistry; -import capstone.relation.workspace.WorkSpace; -import capstone.relation.workspace.repository.WorkSpaceRepository; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class MeetRoomService { - private static final String WORK_KEY = "WORKSPACE_ROOM_PARTICIPANTS"; - private static final String USER_KEY = "USER_ROOM_MAPPING"; - private final UserRepository userRepository; - private final WorkSpaceRepository workSpaceRepository; + private final UserService userService; + private final RedisRepository redisRepository; private final MeetRoomRepository meetRoomRepository; - private final SocketRegistry socketRegistry; - private final RedisTemplate redisTemplate; private final SimpMessagingTemplate simpMessagingTemplate; - private HashOperations>> workspaceRoomParticipants; - //workspaceId, roomId, userIds - private HashOperations userRoomMapping; - - @PostConstruct - protected void init() { - workspaceRoomParticipants = redisTemplate.opsForHash(); - userRoomMapping = redisTemplate.opsForHash(); - } + /** + * 미팅룸을 생성하고 참여합니다. + * @param createRoomDto 생성할 미팅룸 정보 DTO + * @return 참여 응답 DTO + */ @Transactional(readOnly = false) - public JoinResponseDto createAndJoinRoom(CreateRoomDto createRoomDto, Long userId, String workSpaceId) { + public JoinResponseDto createAndJoinRoom(CreateRoomDto createRoomDto) { String roomName = createRoomDto.getRoomName(); - if (roomName == null || roomName.isEmpty()) { + if (roomName == null || roomName.isEmpty()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "회의실 이름을 입력해주세요."); - } - try { - JoinResponseDto joinResponseDto = createAndJoin(workSpaceId, userId.toString(), roomName); - sendRoomList(workSpaceId); - return joinResponseDto; - } catch (AuthException e) { - throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, e.getMessage()); - } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); - } - } - - public boolean isUserInRoom(String userId) { - return userRoomMapping.get(USER_KEY, userId) != null; - } + Long userId = SecurityUtil.getCurrentUserId(); + String workSpaceId = userService.getUserWorkSpaceId(userId); - public RoomInfoDto getRoomInfo(String workspaceId, Long userId) { - if (!isUserInRoom(userId.toString())) { - return new RoomInfoDto(); - } - String roomId = userRoomMapping.get(USER_KEY, userId.toString()); - System.out.println("roomId = " + roomId); - System.out.println("userId = " + userId); - Set userIds = getRoomMembers(workspaceId, Long.parseLong(roomId)); - List userInfoList = new ArrayList<>(); - for (String id : userIds) { - UserInfoDto userInfoDto = new UserInfoDto(); - userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity); - userInfoList.add(userInfoDto); - } - return new RoomInfoDto(true, Long.parseLong(roomId), - meetRoomRepository.findById(Long.parseLong(roomId)).get().getRoomName(), userInfoList); + // 미팅룸을 생성, 참여, 미팅룸 목록을 전송합니다. + Long roomId = createRoom(userId, roomName); + JoinResponseDto joinResponseDto = joinWorkspaceRoom(workSpaceId, userId, roomId); + sendRoomList(workSpaceId); + return joinResponseDto; } - private JoinResponseDto createAndJoin(String workSpaceId, String userId, String roomName) { - WorkSpace workSpace = workSpaceRepository.findById(workSpaceId) - .orElseThrow(() -> new IllegalArgumentException("Invalid workspace ID")); - if (isUserInRoom(userId)) { + /** + * 미팅룸을 생성합니다. + * @param userId 사용자 ID + * @param roomName 미팅룸 이름 + * @return 생성된 미팅룸 ID + */ + private Long createRoom(Long userId, String roomName) { + if (redisRepository.isUserInRoom(userId)) throw new IllegalArgumentException("User is already in the room: " + userId); - } - MeetRoom meetRoom = MeetRoom.builder() .roomName(roomName) .deleted(false) .build(); meetRoomRepository.save(meetRoom); - workSpace.addMeetRoom(meetRoom); - Long roomId = meetRoom.getRoomId(); - return joinWorkspaceRoom(workSpaceId, userId, roomId); + return meetRoom.getRoomId(); + } + + /** + * 미팅룸 정보를 조회합니다. + * @param workspaceId 워크스페이스 ID + * @param userId 사용자 ID + * @return 미팅룸 정보 DTO + */ + public RoomInfoDto getRoomInfo(String workspaceId, Long userId) { + if (!redisRepository.isUserInRoom(userId)) + throw new IllegalArgumentException("User is not in any room: " + userId); + + Long roomId = Long.parseLong(redisRepository.getUserRoomId(userId)); + Set userIds = redisRepository.getRoomMemberIds(workspaceId, roomId); + List userInfoList = userService.getUserInfoList(userIds); + return new RoomInfoDto(true, roomId, getRoomName(roomId), userInfoList); } - public JoinResponseDto joinRoom(Long userId, String workspaceId, Long roomId) { + /** + * 미팅룸 이름을 조회합니다. + * @param roomId 미팅룸 ID + * @return 미팅룸 이름 + */ + private String getRoomName(Long roomId) { + return meetRoomRepository.findById(roomId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid room ID : " + roomId)) + .getRoomName(); + } - //유저가 이미 회의 방에 있는지 확인 - String meetRoom = userRoomMapping.get(USER_KEY, userId.toString()); - if (meetRoom != null) { + /** + * 미팅룸에 참여합니다. 컨트롤러 연결 메서드 + * @param roomId 가입하고자 하는 미팅룸 ID + * @return 참여 응답 DTO + */ + public JoinResponseDto joinRoom(Long roomId) { + Long userId = SecurityUtil.getCurrentUserId(); + String workspaceId = userService.getUserWorkSpaceId(userId); + String meetRoomId = redisRepository.getUserRoomId(userId); + if (meetRoomId != null) throw new IllegalArgumentException("User is already in the room: " + userId); - } - JoinResponseDto joinResponseDto = joinWorkspaceRoom(workspaceId, userId.toString(), roomId); + + JoinResponseDto joinResponseDto = joinWorkspaceRoom(workspaceId, userId, roomId); sendRoomList(workspaceId); return joinResponseDto; } - private JoinResponseDto joinWorkspaceRoom(String workSpaceId, String userId, Long roomId) { + /** + * 미팅룸에 참여합니다. + * 웹소켓으로 변경된 사용자 목록을 전송합니다. + * @param workSpaceId 워크스페이스 ID + * @param userId 사용자 ID + * @param roomId 미팅룸 ID + * @return 참여 응답 DTO + */ + private JoinResponseDto joinWorkspaceRoom(String workSpaceId, Long userId, Long roomId) { MeetRoom meetRoom = meetRoomRepository.findById(roomId) - .orElseThrow(() -> new IllegalArgumentException("Invalid room ID")); - Set userIds = addUserToRoom(workSpaceId, roomId, userId); - List userInfoList = new ArrayList<>(); - for (String id : userIds) { - UserInfoDto userInfoDto = new UserInfoDto(); - userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity); - userInfoList.add(userInfoDto); - } + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Invalid room ID")); + redisRepository.addUserToRoom(workSpaceId, roomId, userId.toString()); + Set userIds = redisRepository.getRoomMemberIds(workSpaceId, roomId); + List userInfoList = userService.getUserInfoList(userIds); sendUserList(workSpaceId, roomId); return new JoinResponseDto(roomId, meetRoom.getRoomName(), userInfoList, (long)userIds.size()); } + /** + * 미팅룸에서 나갑니다. + * 웹소켓으로 변경된 사용자 목록을 전송합니다. + * @param userId 사용자 ID + */ @Transactional(readOnly = false) - public void leaveRoom(Long userId, String workspaceId) { - String meetRoom = userRoomMapping.get(USER_KEY, userId.toString()); - if (meetRoom == null) { - return; - } - removeUserFromRoom(workspaceId, Long.parseLong(meetRoom), userId.toString()); - sendUserList(workspaceId, Long.parseLong(meetRoom)); + public void leaveRoom(Long userId) { + String workspaceId = userService.getUserWorkSpaceId(userId); + String meetRoomId = redisRepository.getUserRoomId(userId); + if (meetRoomId == null) + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "User is not in any room: " + userId); + redisRepository.removeUserFromRoom(workspaceId, Long.parseLong(meetRoomId), userId.toString()); + sendUserList(workspaceId, Long.parseLong(meetRoomId)); sendRoomList(workspaceId); } - private Set addUserToRoom(String workspaceId, Long roomId, String userId) { - HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId); - if (roomParticipants == null) { - roomParticipants = new HashMap<>(); - } - Set users = roomParticipants.get(roomId.toString()); - if (users == null) { - users = new HashSet<>(); - } - users.add(userId); - roomParticipants.put(roomId.toString(), users); - workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants); - userRoomMapping.put(USER_KEY, userId, roomId.toString()); - return users; - } - - private void removeUserFromRoom(String workspaceId, Long roomId, String userId) { - userRoomMapping.delete(USER_KEY, userId); - HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId); - if (roomParticipants == null) { - return; - } - Set userIds = roomParticipants.get(roomId.toString()); - userIds.remove(userId); - - if (userIds.isEmpty()) { - MeetRoom meetRoom = meetRoomRepository.findById(roomId).orElse(null); - if (meetRoom != null) { - meetRoom.setDeleted(true); - meetRoomRepository.save(meetRoom); - } - roomParticipants.remove(roomId.toString()); - } else { - roomParticipants.put(roomId.toString(), userIds); - } - if (roomParticipants.isEmpty()) { - workspaceRoomParticipants.delete(WORK_KEY, workspaceId); - } else { - workspaceRoomParticipants.put(WORK_KEY, workspaceId, roomParticipants); - } - } - - public Set getRoomMembers(String workspaceId, Long roomId) { - HashMap> roomParticipants = workspaceRoomParticipants.get(WORK_KEY, workspaceId.toString()); - if (roomParticipants == null) { - return new HashSet<>(); - } - return roomParticipants.get(roomId.toString()); - } - + /** + * 미팅룸 목록을 웹소켓을 통해 전송합니다. + * 이벤트 : /topic/{workSpaceId}/meetingRoomList + * 전송 데이터 : 워크스페이스에 있는 미팅룸 목록 + * @param workSpaceId 참여중인 워크스페이스 ID + * @return 미팅룸 목록 + */ public MeetingRoomListDto sendRoomList(String workSpaceId) { + // 미팅룸 정보를 조회합니다. Set meetRooms = meetRoomRepository.findAllByWorkSpaceId(workSpaceId); MeetingRoomListDto meetingRoomListDto = new MeetingRoomListDto(); + // 미팅룸 정보를 DTO로 변환합니다. for (MeetRoom meetRoom : meetRooms) { - Long roomId = meetRoom.getRoomId(); MeetingRoomDto meetingRoomDto = new MeetingRoomDto(); - meetingRoomDto.setRoomId(roomId); + meetingRoomDto.setRoomId(meetRoom.getRoomId()); meetingRoomDto.setRoomName(meetRoom.getRoomName()); - Set roomMembers = getRoomMembers(workSpaceId, roomId); - if (roomMembers == null) { - return meetingRoomListDto; - } + + // 미팅룸에 참여한 사용자 수를 조회합니다. + Set roomMembers = redisRepository.getRoomMemberIds(workSpaceId, meetRoom.getRoomId()); meetingRoomDto.setUserCount(roomMembers.size()); - List userInfoList = new ArrayList<>(); - for (String userId : roomMembers) { - UserInfoDto userInfoDto = new UserInfoDto(); - userRepository.findById(Long.parseLong(userId)).ifPresent(userInfoDto::setByUserEntity); - userInfoList.add(userInfoDto); - } + + // 사용자 정보 목록을 저장합니다. + List userInfoList = userService.getUserInfoList(roomMembers); meetingRoomDto.setUserInfoList(userInfoList); + meetingRoomListDto.getMeetingRoomList().add(meetingRoomDto); } + + // 미팅룸 정보를 전송합니다.(웹소켓) simpMessagingTemplate.convertAndSend("/topic/" + workSpaceId + "/meetingRoomList", meetingRoomListDto); return meetingRoomListDto; } - private void sendUserList(String workSpaceId, Long roomId) { - Set userIds = getRoomMembers(workSpaceId, roomId); - List userInfoList = new ArrayList<>(); - for (String userId : userIds) { - UserInfoDto userInfoDto = new UserInfoDto(); - userRepository.findById(Long.parseLong(userId)).ifPresent(userInfoDto::setByUserEntity); - userInfoList.add(userInfoDto); - } - simpMessagingTemplate.convertAndSend("/topic/meetingRoom/" + roomId + "/users", userInfoList); + /** + * 미팅룸 목록을 웹소켓을 통해서 전송합니다.(컨트롤러 연결 메서드) + * 이벤트 : /topic/meetingRoom/{roomId}/users + * 전송 데이터 : 사용자 정보 리스트 + * @return 미팅룸 목록 + */ + public MeetingRoomListDto sendRoomList() { + Long userId = SecurityUtil.getCurrentUserId(); + String workSpaceId = userService.getUserWorkSpaceId(userId); + return sendRoomList(workSpaceId); } - public void sendErrorMessage(SimpMessageHeaderAccessor headerAccessor, String message, String destination, - int status) { - Long userId = (Long)headerAccessor.getSessionAttributes().get("userId"); - String socketId = socketRegistry.getSocketId(userId.toString()); - simpMessagingTemplate.convertAndSendToUser(socketId, destination, - ResponseEntity.status(status).body(message)); + /** + * 미팅룸에 참여한 사용자 목록을 웹소켓을 통해서 전송합니다. + * 이벤트 : /topic/meetingRoom/{roomId}/users + * 전송 데이터 : 사용자 정보 리스트 + * @param workSpaceId 워크스페이스 ID + * @param roomId 미팅룸 ID + */ + private void sendUserList(String workSpaceId, Long roomId) { + Set userIds = redisRepository.getRoomMemberIds(workSpaceId, roomId); + List userInfoList = userService.getUserInfoList(userIds); + simpMessagingTemplate.convertAndSend("/topic/meetingRoom/" + roomId + "/users", userInfoList); } } diff --git a/src/main/java/capstone/relation/user/UserService.java b/src/main/java/capstone/relation/user/UserService.java index fe8ff03..d1f4570 100644 --- a/src/main/java/capstone/relation/user/UserService.java +++ b/src/main/java/capstone/relation/user/UserService.java @@ -1,6 +1,9 @@ package capstone.relation.user; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; @@ -72,4 +75,26 @@ public void leaveWorkspace() { user.setWorkSpace(null); userRepository.save(user); } + + public String getUserWorkSpaceId(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")) + .getWorkSpace() + .getId(); + } + + /** + * 사용자 Id 목록을 받아서 사용자 정보 목록을 반환합니다. + * @param userIds 사용자 Id 목록 + * @return 사용자 정보 목록 + */ + public List getUserInfoList(Set userIds) { + List userInfoList = new ArrayList<>(); + for (String id : userIds) { + UserInfoDto userInfoDto = new UserInfoDto(); + userRepository.findById(Long.parseLong(id)).ifPresent(userInfoDto::setByUserEntity); + userInfoList.add(userInfoDto); + } + return userInfoList; + } } diff --git a/src/main/java/capstone/relation/user/controller/UserController.java b/src/main/java/capstone/relation/user/controller/UserController.java index 85fd717..2362aec 100644 --- a/src/main/java/capstone/relation/user/controller/UserController.java +++ b/src/main/java/capstone/relation/user/controller/UserController.java @@ -28,7 +28,7 @@ public UserInfoDto getInfo() { } @GetMapping("/room/info") - @Operation(summary = "사용자 방 정보 조회", description = "현재 로그인한 사용자의 방 정보를 조회합니다.") + @Operation(summary = "사용자 회의 방 정보 조회", description = "현재 로그인한 사용자의 회의 방 정보를 조회합니다.") public RoomInfoDto getRoomInfo() { User user = userService.getUserEntity(); Long userId = user.getId(); diff --git a/src/main/java/capstone/relation/user/domain/User.java b/src/main/java/capstone/relation/user/domain/User.java index 32d1034..b9d837a 100644 --- a/src/main/java/capstone/relation/user/domain/User.java +++ b/src/main/java/capstone/relation/user/domain/User.java @@ -36,6 +36,7 @@ public User(Long id, String profileImage, String userName, String email, String this.provider = provider; this.email = email; this.role = role; + this.id = id; } @Id diff --git a/src/main/java/capstone/relation/user/dto/UserInfoDto.java b/src/main/java/capstone/relation/user/dto/UserInfoDto.java index f469b2e..11e52d7 100644 --- a/src/main/java/capstone/relation/user/dto/UserInfoDto.java +++ b/src/main/java/capstone/relation/user/dto/UserInfoDto.java @@ -4,8 +4,10 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor public class UserInfoDto { @Schema(description = "사용자 ID", example = "1234567890") private Long userId; @@ -16,6 +18,13 @@ public class UserInfoDto { @Schema(description = "사용자 이메일", example = "wnddms12345@naver.com") private String email; + public UserInfoDto(Long userId, String userName, String email, String userImage) { + this.userId = userId; + this.userName = userName; + this.email = email; + this.userImage = userImage; + } + @Hidden public void setByUserEntity(User user) { this.userId = user.getId(); diff --git a/src/main/java/capstone/relation/websocket/WebSocketEventListener.java b/src/main/java/capstone/relation/websocket/WebSocketEventListener.java index 9c70a5a..cbf119e 100644 --- a/src/main/java/capstone/relation/websocket/WebSocketEventListener.java +++ b/src/main/java/capstone/relation/websocket/WebSocketEventListener.java @@ -18,19 +18,15 @@ public class WebSocketEventListener { @EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); - // String sessionId = headerAccessor.getSessionId(); String socketId = headerAccessor.getUser().getName(); Long userId = (Long)headerAccessor.getSessionAttributes().get("userId"); - String workSpaceId = (String)headerAccessor.getSessionAttributes().get("workSpaceId"); System.out.println("User Disconnected : " + userId); - System.out.println("socketId : " + socketId); if (userId == null) { return; } - meetRoomService.leaveRoom(userId, workSpaceId); + meetRoomService.leaveRoom(userId); System.out.println("register SocketId" + socketRegistry.getSocketId(userId.toString())); - if (socketId == socketRegistry.getSocketId(userId.toString())) { + if (socketId == socketRegistry.getSocketId(userId.toString())) socketRegistry.unregisterSession(userId.toString()); - } } } diff --git a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java b/src/main/java/capstone/relation/websocket/signaling/SignalingController.java similarity index 94% rename from src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java rename to src/main/java/capstone/relation/websocket/signaling/SignalingController.java index 3d0b987..9d3f1a1 100644 --- a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingController.java +++ b/src/main/java/capstone/relation/websocket/signaling/SignalingController.java @@ -1,4 +1,4 @@ -package capstone.relation.websocket.signaling.controller; +package capstone.relation.websocket.signaling; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -18,7 +18,6 @@ public class SignalingController { @MessageMapping("/ice/{roomId}") public void ice(@DestinationVariable String roomId, IceDto iceDto, SimpMessageHeaderAccessor headerAccessor) { - System.out.println("ice"); Long userId = (Long)headerAccessor.getSessionAttributes().get("userId"); signalingService.sendIce(roomId, iceDto, userId); } diff --git a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java b/src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java similarity index 99% rename from src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java rename to src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java index d09626e..b929866 100644 --- a/src/main/java/capstone/relation/websocket/signaling/controller/SignalingDocumentController.java +++ b/src/main/java/capstone/relation/websocket/signaling/SignalingDocumentController.java @@ -1,4 +1,4 @@ -package capstone.relation.websocket.signaling.controller; +package capstone.relation.websocket.signaling; import java.util.List; diff --git a/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java b/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java index 915e8a4..1df17e0 100644 --- a/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java +++ b/src/main/java/capstone/relation/websocket/signaling/dto/SdpResponseDto.java @@ -2,9 +2,12 @@ import capstone.relation.user.dto.UserInfoDto; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor @Schema(description = "offer 또는 answer를 받을 때 사용하는 DTO") public class SdpResponseDto { @Schema(description = "메시지를 보낸 사용자 정보(즉 자신이 아닌 통신하고 있는 유저 id)") @@ -15,4 +18,11 @@ public class SdpResponseDto { @Schema(description = "메시지 타입 \n" + "ScreenShare | Video", example = "ScreenShare") private SignalMessageType type; + + @Builder + public SdpResponseDto(UserInfoDto userInfo, SdpDto sessionDescription, SignalMessageType type) { + this.userInfo = userInfo; + this.sessionDescription = sessionDescription; + this.type = type; + } } diff --git a/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java b/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java index 68a9474..e12cce8 100644 --- a/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java +++ b/src/main/java/capstone/relation/websocket/signaling/service/SignalingService.java @@ -13,40 +13,58 @@ @Service @RequiredArgsConstructor public class SignalingService { + private final UserService userService; private final SocketRegistry socketRegistry; - private final SimpMessagingTemplate simpMessagingTemplate; - private final UserService userService; - public void sendOffer(String roomId, SdpMessageDto sdpMessageDto, Long myId) { - System.out.println("sendOffer"); + /** + * Offer 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.) + * /user/queue/offer/{roomId} 로 전송합니다. + * @param roomId 참여중인 화상회의방 ID + * @param sdpMessageDto Offer 메시지 DTO + * @param senderId 보내는 사람 ID (내 Id) + */ + public void sendOffer(String roomId, SdpMessageDto sdpMessageDto, Long senderId) { String destId = sdpMessageDto.getUserId(); String socketId = socketRegistry.getSocketId(destId); - SdpResponseDto sdpResponseDto = new SdpResponseDto(); - sdpResponseDto.setUserInfo(userService.getUserInfo(myId)); - sdpResponseDto.setSessionDescription(sdpMessageDto.getSessionDescription()); - sdpResponseDto.setType(sdpMessageDto.getType()); + + SdpResponseDto sdpResponseDto = SdpResponseDto.builder() + .userInfo(userService.getUserInfo(senderId)) + .sessionDescription(sdpMessageDto.getSessionDescription()) + .type(sdpMessageDto.getType()) + .build(); simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/offer/" + roomId, sdpResponseDto); } - public void sendAnswer(String roomId, SdpMessageDto sdpMessageDto, Long myId) { - System.out.println("sendAnswer"); + /** + * Answer 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.) + * /user/queue/answer/{roomId} 로 전송합니다. + * @param roomId 참여중인 화상회의방 ID + * @param sdpMessageDto Answer 메시지 DTO + * @param senderId 보내는 사람 ID (내 Id) + */ + public void sendAnswer(String roomId, SdpMessageDto sdpMessageDto, Long senderId) { String socketId = socketRegistry.getSocketId(sdpMessageDto.getUserId()); - SdpResponseDto sdpResponseDto = new SdpResponseDto(); - sdpResponseDto.setUserInfo(userService.getUserInfo(myId)); - sdpResponseDto.setSessionDescription(sdpMessageDto.getSessionDescription()); - sdpResponseDto.setType(sdpMessageDto.getType()); + SdpResponseDto sdpResponseDto = SdpResponseDto.builder() + .userInfo(userService.getUserInfo(senderId)) + .sessionDescription(sdpMessageDto.getSessionDescription()) + .type(sdpMessageDto.getType()) + .build(); simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/answer/" + roomId, sdpResponseDto); } - public void sendIce(String roomId, IceDto iceDto, Long myId) { - System.out.println("sendIce"); + /** + * Ice 메시지를 상대방에게 전송합니다.(목적지는 메시지를 확인해서 소켓 ID를 찾아서 전송합니다.) + * @param roomId 참여중인 화상회의방 ID + * @param iceDto Ice 메시지 DTO + * @param senderId 보내는 사람 ID (내 Id) + */ + public void sendIce(String roomId, IceDto iceDto, Long senderId) { String destId = iceDto.getUserId(); String socketId = socketRegistry.getSocketId(destId); - iceDto.setUserId(myId.toString()); // 보내는 사람 ID로 갈아 껴줌. + iceDto.setUserId(senderId.toString()); // 보내는 사람 ID로 갈아 껴줌. iceDto.setType(iceDto.getType()); simpMessagingTemplate.convertAndSendToUser(socketId, "/queue/ice/" + roomId, iceDto); } - } diff --git a/src/main/java/capstone/relation/workspace/WorkSpace.java b/src/main/java/capstone/relation/workspace/WorkSpace.java index 05f4099..c23842b 100644 --- a/src/main/java/capstone/relation/workspace/WorkSpace.java +++ b/src/main/java/capstone/relation/workspace/WorkSpace.java @@ -45,10 +45,6 @@ public void addUser(User user) { user.setInvitedWorkspaceId(""); } - public void setId(String id) { - this.id = id; - } - public void setName(String name) { this.name = name; } diff --git a/src/test/java/capstone/relation/CustomInitListenerTest.java b/src/test/java/capstone/relation/CustomInitListenerTest.java new file mode 100644 index 0000000..67a6c00 --- /dev/null +++ b/src/test/java/capstone/relation/CustomInitListenerTest.java @@ -0,0 +1,26 @@ +package capstone.relation; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import capstone.relation.workspace.school.service.SchoolService; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(classes = {TestCustomInitConfig.class}) // 테스트 전용 설정 클래스 추가 +@ActiveProfiles("test") +public class CustomInitListenerTest { + + @Autowired + private SchoolService schoolService; + + @Test + public void testCustomInitListener() { + // 테스트 로직 + // CustomInitListener가 호출되어 schoolService.initSchool() 메서드가 실행되는지 확인 + schoolService.initSchool(); + } +} diff --git a/src/test/java/capstone/relation/TestCustomInitConfig.java b/src/test/java/capstone/relation/TestCustomInitConfig.java new file mode 100644 index 0000000..92b0ca4 --- /dev/null +++ b/src/test/java/capstone/relation/TestCustomInitConfig.java @@ -0,0 +1,18 @@ +package capstone.relation; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; + +import capstone.relation.common.initializer.CustomInitListener; +import capstone.relation.workspace.school.service.SchoolService; + +@TestConfiguration +@Profile("test") +public class TestCustomInitConfig { + + @Bean + public CustomInitListener customInitListener(SchoolService schoolService) { + return new CustomInitListener(schoolService); + } +} diff --git a/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java b/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java new file mode 100644 index 0000000..fc12bf7 --- /dev/null +++ b/src/test/java/capstone/relation/meeting/controller/MeetRoomControllerTest.java @@ -0,0 +1,79 @@ +package capstone.relation.meeting.controller; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import capstone.relation.meeting.dto.request.CreateRoomDto; +import capstone.relation.meeting.dto.response.JoinResponseDto; +import capstone.relation.meeting.service.MeetRoomService; +import capstone.relation.security.WithMockCustomUser; +import capstone.relation.user.dto.UserInfoDto; + +@ExtendWith(SpringExtension.class) +@DisplayName("회의방 컨트롤러 테스트") +class MeetRoomControllerTest { + + @InjectMocks + private MeetRoomController meetRoomController; + + @Mock + private MeetRoomService meetRoomService; + + private MockMvc mockMvc; + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(meetRoomController).build(); + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("방 생성") + void testCreateRoom() throws Exception { + CreateRoomDto createRoomDto = new CreateRoomDto("회의방"); + List userInfoList = new ArrayList<>(); + UserInfoDto userInfoDto = new UserInfoDto(1L, "이름", "이메일", "사진"); + userInfoList.add(userInfoDto); + JoinResponseDto joinResponseDto = new JoinResponseDto(1L, "회의방", userInfoList, 1L); + + when(meetRoomService.createAndJoinRoom(any(CreateRoomDto.class))) + .thenReturn(joinResponseDto); + + mockMvc.perform(post("/meet/room/create") + .contentType("application/json") + .content(objectMapper.writeValueAsString(createRoomDto))) // CreateRoomDto 객체를 JSON 문자열로 변환 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomId").value(joinResponseDto.getRoomId())) + .andExpect(jsonPath("$.roomName").value(joinResponseDto.getRoomName())) + .andExpect(jsonPath("$.userInfoList[0].userId").value(userInfoDto.getUserId())) + .andExpect(jsonPath("$.userInfoList[0].userName").value(userInfoDto.getUserName())) + .andExpect(jsonPath("$.userInfoList[0].email").value(userInfoDto.getEmail())) + .andExpect(jsonPath("$.userInfoList[0].userImage").value(userInfoDto.getUserImage())) + .andExpect(jsonPath("$.userCount").value(joinResponseDto.getUserCount())); + } + + @Test + @DisplayName("방 나가기 테스트") + @WithMockCustomUser + void testLeaveRoom() throws Exception { + mockMvc.perform(post("/meet/room/leave")) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java b/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java new file mode 100644 index 0000000..c4a09f5 --- /dev/null +++ b/src/test/java/capstone/relation/meeting/integration/MeetRoomIntegrationTest.java @@ -0,0 +1,224 @@ +package capstone.relation.meeting.integration; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import capstone.relation.meeting.domain.MeetRoom; +import capstone.relation.meeting.dto.request.CreateRoomDto; +import capstone.relation.meeting.repository.MeetRoomRepository; +import capstone.relation.meeting.repository.RedisRepository; +import capstone.relation.security.WithMockCustomUser; +import capstone.relation.user.domain.Role; +import capstone.relation.user.domain.User; +import capstone.relation.user.repository.UserRepository; +import capstone.relation.workspace.WorkSpace; +import capstone.relation.workspace.repository.WorkSpaceRepository; +import capstone.relation.workspace.school.domain.School; +import capstone.relation.workspace.school.respository.SchoolRepository; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@DisplayName("회의방 통합 테스트") +public class MeetRoomIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + UserRepository userRepository; + + @Autowired + WorkSpaceRepository workSpaceRepository; + + @Autowired + SchoolRepository schoolRepository; + + @Autowired + MeetRoomRepository meetRoomRepository; + + @MockBean + private RedisRepository redisRepository; + + private WorkSpace testWorkSpace; + private School testSchool; + + @BeforeEach + void setup() { + redisRepository.deleteAll(); // Redis 저장소 초기화 + testSchool = new School("서울캠", "과기대", "4년제", "링크", "학교소개", "학교위치", "학교지역", "학교종류", new HashSet<>()); + schoolRepository.save(testSchool); + + testWorkSpace = new WorkSpace(); + testWorkSpace.setName("testWorkSpace"); + testWorkSpace.setSchool(testSchool); + workSpaceRepository.save(testWorkSpace); + + addTestUser("testUser1"); + addTestUser("testUser2"); + addTestUser("testUser3"); + + // RedisRepository 모킹 + when(redisRepository.isUserInRoom(any())).thenReturn(false); + when(redisRepository.getUserRoomId(1L)).thenReturn(null); + when(redisRepository.getRoomMemberIds(testWorkSpace.getId(), 1L)).thenReturn(new HashSet<>(Set.of("1"))); + when(redisRepository.getRoomMemberIds(testWorkSpace.getId(), 2L)).thenReturn(new HashSet<>(Set.of("1"))); + } + + private void addTestUser(String username) { + User user = User.builder().userName(username) + .email(username + "@naver.com") + .profileImage( + "https://lh3.googleusercontent.com/ogw/AF2bZyhqowurXq6imx61oPHn5G_c6OIEnucOyJanitxYGFUI498=s32-c-mo") + .role(Role.USER) + .provider("naver") + .build(); + workSpaceRepository.save(testWorkSpace); + user.setWorkSpace(testWorkSpace); + userRepository.save(user); + } + + @Test + @DisplayName("통합 테스트 : 새로운 방 생성을 할 수 있다.") + @WithMockCustomUser + @DirtiesContext + void createRoom() throws Exception { + // given + CreateRoomDto createRoomDto = new CreateRoomDto("New Room"); + String requestJson = objectMapper.writeValueAsString(createRoomDto); + + // when & then + mockMvc.perform(post("/meet/room/create") + .contentType("application/json") + .content(requestJson)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomId").isNumber()) + .andExpect(jsonPath("$.roomName").value("New Room")) + .andExpect(jsonPath("$.userInfoList").isArray()) + .andExpect(jsonPath("$.userCount").value(1)); + } + + @Test + @DisplayName("통합 테스트 : 존재하는 회의방에 참여할 수 있다.") + @WithMockCustomUser + @DirtiesContext + void joinRoom() throws Exception { + // given + MeetRoom meetRoom = MeetRoom.builder() + .roomName("채팅방") + .deleted(false) + .build(); + meetRoomRepository.save(meetRoom); + // when & then + mockMvc.perform(post("/meet/room/join/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomId").isNumber()) + .andExpect(jsonPath("$.roomName").value("채팅방")) + .andExpect(jsonPath("$.userInfoList").isArray()) + .andExpect(jsonPath("$.userCount").value(1)); + } + + @Test + @DisplayName("통합 테스트 : 존재하지 않는 회의방에 참여할 수 없다.") + @DirtiesContext + @WithMockCustomUser + void joinRoomFail() throws Exception { + // when & then + mockMvc.perform(post("/meet/room/join/1")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("통합 테스트 : 회의방에서 나갈 수 있다.") + @WithMockCustomUser(id = 2L) + @DirtiesContext + void leaveRoom() throws Exception { + // given + MeetRoom meetRoom = MeetRoom.builder() + .roomName("채팅방1") + .deleted(false) + .build(); + meetRoomRepository.save(meetRoom); + MeetRoom meetRoom2 = MeetRoom.builder() + .roomName("채팅방2") + .deleted(false) + .build(); + meetRoomRepository.save(meetRoom2); + // join 시에 필요한 모킹 설정 + when(redisRepository.getUserRoomId(anyLong())).thenReturn(null); + when(redisRepository.getRoomMemberIds(any(), anyLong())).thenReturn(new HashSet<>(Set.of("2"))); + // when & then + mockMvc.perform(post("/meet/room/join/2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.roomId").isNumber()) + .andExpect(jsonPath("$.roomName").value("채팅방2")) + .andExpect(jsonPath("$.userInfoList").isArray()) + .andExpect(jsonPath("$.userCount").value(1)); + // leave 시에 필요한 모킹 설정 + when(redisRepository.getUserRoomId(anyLong())).thenReturn(meetRoom2.getRoomId().toString()); + + mockMvc.perform(post("/meet/room/leave")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("통합 테스트 : 회의방 목록을 요청할 수 있다.") + @WithMockCustomUser + @DirtiesContext + void requestRoomList() throws Exception { + // given + MeetRoom meetRoom = MeetRoom.builder() + .roomName("채팅방1") + .deleted(false) + .build(); + meetRoomRepository.save(meetRoom); + MeetRoom meetRoom2 = MeetRoom.builder() + .roomName("채팅방2") + .deleted(false) + .build(); + meetRoomRepository.save(meetRoom2); + + testWorkSpace.addMeetRoom(meetRoom); + testWorkSpace.addMeetRoom(meetRoom2); + workSpaceRepository.save(testWorkSpace); + + // join 시에 필요한 모킹 설정 + when(redisRepository.getUserRoomId(anyLong())).thenReturn(null); + when(redisRepository.getRoomMemberIds(any(), anyLong())).thenReturn(new HashSet<>(Set.of("1"))); + // when & then + mockMvc.perform(get("/meet/room/list")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.meetingRoomList").isArray()) + .andExpect(jsonPath("$.meetingRoomList[0].roomId").isNumber()) + .andExpect(jsonPath("$.meetingRoomList[0].roomName").value("채팅방1")) + .andExpect(jsonPath("$.meetingRoomList[0].userCount").value(1)) + .andExpect(jsonPath("$.meetingRoomList[0].userInfoList").isArray()) + .andExpect(jsonPath("$.meetingRoomList[1].roomId").isNumber()) + .andExpect(jsonPath("$.meetingRoomList[1].roomName").value("채팅방2")) + .andExpect(jsonPath("$.meetingRoomList[1].userCount").value(1)) + .andExpect(jsonPath("$.meetingRoomList[1].userInfoList").isArray()); + } +} diff --git a/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java b/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java index 2f000a1..cf14c6f 100644 --- a/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java +++ b/src/test/java/capstone/relation/meeting/service/MeetRoomServiceTest.java @@ -3,176 +3,137 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Optional; -import java.util.Set; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.HashOperations; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.http.HttpStatus; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.server.ResponseStatusException; import capstone.relation.meeting.domain.MeetRoom; import capstone.relation.meeting.dto.request.CreateRoomDto; import capstone.relation.meeting.dto.response.JoinResponseDto; import capstone.relation.meeting.dto.response.MeetingRoomListDto; import capstone.relation.meeting.repository.MeetRoomRepository; -import capstone.relation.user.domain.Role; -import capstone.relation.user.domain.User; -import capstone.relation.user.repository.UserRepository; -import capstone.relation.websocket.SocketRegistry; +import capstone.relation.meeting.repository.RedisRepository; +import capstone.relation.security.WithMockCustomUser; +import capstone.relation.user.UserService; import capstone.relation.workspace.WorkSpace; import capstone.relation.workspace.repository.WorkSpaceRepository; -@ExtendWith(MockitoExtension.class) +@ExtendWith(SpringExtension.class) class MeetRoomServiceTest { @InjectMocks private MeetRoomService meetRoomService; @Mock - private SimpMessagingTemplate simpMessagingTemplate; + private UserService mockUserService; @Mock - private SocketRegistry socketRegistry; + private SimpMessagingTemplate mockSimpMessagingTemplate; @Mock - private WorkSpaceRepository workSpaceRepository; + private WorkSpaceRepository mockWorkSpaceRepository; @Mock - private MeetRoomRepository meetRoomRepository; - - @Mock - private RedisTemplate redisTemplate; - + private MeetRoomRepository mockMeetRoomRepository; + @Mock - private HashOperations>> workspaceRoomParticipants; - //workspaceId, roomId, userIds - @Mock - private HashOperations mockUserRoomMapping; + private RedisRepository mockRedisRepository; - @Mock - private UserRepository userRepository; - - @BeforeEach - void setUp() { - // RedisTemplate의 opsForHash를 모킹하여 hashOperations를 반환하도록 설정합니다. - when(redisTemplate.opsForHash()).thenReturn((HashOperations)workspaceRoomParticipants) - .thenReturn(mockUserRoomMapping); - // MeetingService의 init() 메서드를 명시적으로 호출합니다. - meetRoomService.init(); - } - - @DisplayName("회의 방을 생성할 수 있다.") + @DisplayName("회의 방을 생성하고 가입할 수 있다.") + @WithMockCustomUser @Test - void createRoom() { + void createAndJoinRoom() { // given CreateRoomDto createRoomDto = new CreateRoomDto("테스트 방이름"); - SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(); - Map sessionAttributes = new HashMap<>(); - sessionAttributes.put("userId", 1L); - sessionAttributes.put("workSpaceId", "workspace-1"); - headerAccessor.setSessionAttributes(sessionAttributes); - - given(workSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace())); - given(meetRoomRepository.save(any(MeetRoom.class))).willAnswer(invocation -> { + + given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1"); + given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace())); + given(mockMeetRoomRepository.save(any(MeetRoom.class))).willAnswer(invocation -> { MeetRoom meetRoom = invocation.getArgument(0); meetRoom.setRoomId(1L); return meetRoom; }); - given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(new HashMap<>()); - given(meetRoomRepository.findById(1L)).willReturn( + given(mockMeetRoomRepository.findById(1L)).willReturn( Optional.of(MeetRoom.builder().roomId(1L).roomName("테스트 방이름").build())); // when - JoinResponseDto joinResponse = meetRoomService.createAndJoinRoom(createRoomDto, 1L, "workspace-1"); + JoinResponseDto joinResponse = meetRoomService.createAndJoinRoom(createRoomDto); // then - verify(simpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"), + verify(mockSimpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"), any(MeetingRoomListDto.class)); assertThat(joinResponse).isNotNull(); assertThat(joinResponse.getRoomName()).isEqualTo("테스트 방이름"); assertThat(joinResponse.getRoomId()).isEqualTo(1L); } - @DisplayName("회의 방 목록을 받아올 수 있다.") + @DisplayName("잘못된 회의실 이름으로 회의 방을 생성하려고 할 때 예외가 발생한다.") @Test - void getRoomList() { + public void createAndJoinRoomWithInvalidRoomName() { // given - WorkSpace workSpace = new WorkSpace(); - workSpace.setId("workspace-1"); - String workSpaceId = workSpace.getId(); - MeetRoom meetRoom1 = MeetRoom.builder().roomId(1L).roomName("회의실1").deleted(false).workSpace(workSpace).build(); - MeetRoom meetRoom2 = MeetRoom.builder().roomId(2L).roomName("회의실2").deleted(false).workSpace(workSpace).build(); - - Set meetRooms = new HashSet<>(); - meetRooms.add(meetRoom1); - meetRooms.add(meetRoom2); - Set userIds = new HashSet<>(); - userIds.add("1"); - userIds.add("2"); - HashMap> mockParti = new HashMap<>(); - mockParti.put("1", userIds); - mockParti.put("2", userIds); - given(meetRoomRepository.findAllByWorkSpaceId(workSpaceId)).willReturn(meetRooms); - given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(mockParti); + CreateRoomDto createRoomDto = new CreateRoomDto(""); + + // when & then + assertThatThrownBy(() -> meetRoomService.createAndJoinRoom(createRoomDto)) + .isInstanceOf(ResponseStatusException.class) + .satisfies(exception -> { + ResponseStatusException ex = (ResponseStatusException)exception; + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(ex.getReason()).isEqualTo("회의실 이름을 입력해주세요."); + }); + } + + @DisplayName("이미 회의실에 참여한 사용자가 다시 참여하려고 할 때 예외가 발생한다.") + @WithMockCustomUser + @Test + public void createAndJoinRoomWithAlreadyJoinedUser() { + // given + CreateRoomDto createRoomDto = new CreateRoomDto("테스트 방이름"); + + given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1"); + given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace())); + given(mockRedisRepository.isUserInRoom(1L)).willReturn(true); + + // when & then + assertThatThrownBy(() -> meetRoomService.createAndJoinRoom(createRoomDto)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("User is already in the room: 1"); + } + + @DisplayName("워크스페이스에 참여한 모든 유저에게 회의실 목록을 전송할 수 있다.") + @Test + public void sendRoomList() { + // given + given(mockWorkSpaceRepository.findById("workspace-1")).willReturn(Optional.of(new WorkSpace())); // when - meetRoomService.sendRoomList(workSpaceId); + meetRoomService.sendRoomList("workspace-1"); // then - ArgumentCaptor roomListCaptor = ArgumentCaptor.forClass(MeetingRoomListDto.class); - verify(simpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"), - roomListCaptor.capture()); - - MeetingRoomListDto roomList = roomListCaptor.getValue(); - assertThat(roomList).isNotNull(); - assertThat(roomList.getMeetingRoomList()).hasSize(2); - assertThat(roomList.getMeetingRoomList()).anyMatch( - room -> room.getRoomId().equals(1L) && room.getRoomName().equals("회의실1")); - assertThat(roomList.getMeetingRoomList()).anyMatch( - room -> room.getRoomId().equals(2L) && room.getRoomName().equals("회의실2")); + verify(mockSimpMessagingTemplate, times(1)).convertAndSend(eq("/topic/workspace-1/meetingRoomList"), + any(MeetingRoomListDto.class)); } - @DisplayName("회의 방에 참여할 수 있다.") + @DisplayName("회의실을 나갈 수 있다.") + @WithMockCustomUser @Test - void joinRoom() { + public void leaveRoom() { // given - SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(); - Map sessionAttributes = new HashMap<>(); - sessionAttributes.put("userId", 1L); - sessionAttributes.put("workSpaceId", "workspace-1"); - headerAccessor.setSessionAttributes(sessionAttributes); - - WorkSpace workSpace = new WorkSpace(); - workSpace.setId("workspace-1"); - MeetRoom meetRoom1 = MeetRoom.builder().roomId(1L).roomName("회의실1").deleted(false).workSpace(workSpace).build(); - given(meetRoomRepository.findById(1L)).willReturn(Optional.of(meetRoom1)); - given(userRepository.findById(1L)).willReturn(Optional.of( - User.builder() - .id(1L) - .email("wnddms12345@naver.com") - .profileImage("https://avatars.githubusercontent.com/u/77449538?v=4") - .userName("김민수") - .provider("github") - .role(Role.USER) - .build())); - given(workspaceRoomParticipants.get(anyString(), anyString())).willReturn(new HashMap<>()); + given(mockUserService.getUserWorkSpaceId(1L)).willReturn("workspace-1"); + given(mockRedisRepository.isUserInRoom(1L)).willReturn(true); + given(mockRedisRepository.getUserRoomId(1L)).willReturn("1"); + // when - JoinResponseDto joinResponseDto = meetRoomService.joinRoom(1L, "workspace-1", 1L); + meetRoomService.leaveRoom(1L); // then - assertThat(joinResponseDto).isNotNull(); - assertThat(joinResponseDto.getRoomId()).isEqualTo(1L); - assertThat(joinResponseDto.getRoomName()).isEqualTo("회의실1"); + verify(mockRedisRepository, times(1)).removeUserFromRoom("workspace-1", 1L, "1"); } } diff --git a/src/test/java/capstone/relation/security/WithMockCustomUser.java b/src/test/java/capstone/relation/security/WithMockCustomUser.java new file mode 100644 index 0000000..dd1b207 --- /dev/null +++ b/src/test/java/capstone/relation/security/WithMockCustomUser.java @@ -0,0 +1,14 @@ +package capstone.relation.security; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithMockCustomUser { + long id() default 1L; + + String role() default "USER"; +} \ No newline at end of file diff --git a/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java b/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 0000000..699087b --- /dev/null +++ b/src/test/java/capstone/relation/security/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,22 @@ +package capstone.relation.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +import capstone.relation.api.auth.jwt.SecurityUser; + +public class WithMockCustomUserSecurityContextFactory + implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockCustomUser customUser) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + SecurityUser securityUser = SecurityUser.of(customUser.id(), customUser.role()); + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(securityUser, null, + securityUser.getAuthorities()); + context.setAuthentication(auth); + return context; + } +} \ No newline at end of file diff --git a/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java b/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java new file mode 100644 index 0000000..8a3ea3c --- /dev/null +++ b/src/test/java/capstone/relation/websocket/signaling/SignalingIntegrationTest.java @@ -0,0 +1,244 @@ +package capstone.relation.websocket.signaling; + +import static capstone.relation.user.domain.Role.*; +import static capstone.relation.websocket.signaling.dto.SignalMessageType.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import java.lang.reflect.Type; +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; + +import capstone.relation.api.auth.jwt.TokenProvider; +import capstone.relation.user.domain.User; +import capstone.relation.user.repository.UserRepository; +import capstone.relation.websocket.signaling.dto.IceDto; +import capstone.relation.websocket.signaling.dto.SdpDto; +import capstone.relation.websocket.signaling.dto.SdpMessageDto; +import capstone.relation.websocket.signaling.dto.SdpResponseDto; +import capstone.relation.workspace.WorkSpace; +import capstone.relation.workspace.repository.WorkSpaceRepository; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") //테스트 프로필 활성화. +@ExtendWith(SpringExtension.class) //단위 테스트에 공통적으로 사용할 확장 기능을 선언 +public class SignalingIntegrationTest { + @LocalServerPort + private int port; + + private String WEBSOCKET_URI; + private String WEBSOCKET_TOPIC; + + private WebSocketStompClient stompClient; + private User sender; + private User receiver; + + private Date accessTokenExpiredDate; + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private WorkSpaceRepository workSpaceRepository; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + this.WEBSOCKET_URI = "ws://localhost:" + port + "/ws-chat"; + this.WEBSOCKET_TOPIC = "/app"; + this.stompClient = new WebSocketStompClient(new StandardWebSocketClient()); + this.stompClient.setMessageConverter(new MappingJackson2MessageConverter()); + sender = User.builder() + .email("wnddms12345@gmail.com") + .profileImage("profileImage") + .provider("local") + .userName("senderName") + .role(USER) + .build(); + WorkSpace workSpace = new WorkSpace(); + workSpace.setName("workSpaceName"); + workSpaceRepository.save(workSpace); + sender.setWorkSpace(workSpace); + userRepository.save(sender); + + receiver = User.builder() + .email("wnddms12345@gmail.com") + .profileImage("profileImage") + .provider("local") + .userName("receiverName") + .role(USER) + .build(); + receiver.setWorkSpace(workSpace); + userRepository.save(receiver); + + accessTokenExpiredDate = new Date(new Date().getTime() + 10000000L); + } + + private StompSession connectWebSocket(User user) throws + InterruptedException, + ExecutionException { + + System.out.println("유저 ID: " + user.getId()); + String token = tokenProvider.generateAccessToken(user, accessTokenExpiredDate); + System.out.println("임시 엑세스 토큰 생성: " + token); + + //연결 시 사용하는 JWT 설정 + WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); + headers.add("Authorization", "Bearer " + token); + //STOMP 사용시 사용하는 JWT 설정 + StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.add("Authorization", "Bearer " + token); + + //STOMP 연결 + StompSession session = stompClient.connectAsync(WEBSOCKET_URI, headers, stompHeaders, + new StompSessionHandlerAdapter() { + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + System.out.println("STOMP 연결 성공"); + } + }).get(); + + return session; + } + + @Test + @DisplayName("소켓으로 연결된 유저에게 ice 메시지 전송할 수 있다.") + public void testIce() throws InterruptedException, ExecutionException, TimeoutException { + BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + + IceDto iceDto = new IceDto(); + iceDto.setType(ScreenShare); + iceDto.setCandidate("candidate"); + iceDto.setSdpMid("sdpMid"); + iceDto.setSdpMLineIndex("sdpMLineIndex"); + iceDto.setUserId(receiver.getId().toString()); + //유저 2명 연결 1이 송신 2가 수신. + StompSession senderSession = connectWebSocket(sender); + StompSession receiverSession = connectWebSocket(receiver); + receiverSession.subscribe("/user/queue/ice/123", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + System.out.println("구독 시작: /user/queue/ice/123"); + return IceDto.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + System.out.println("ice 응답값 : " + payload); + messageQueue.offer((IceDto)payload); + } + }); + StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.setDestination(WEBSOCKET_TOPIC + "/ice/123"); + + senderSession.send(stompHeaders, iceDto); + + IceDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS); + assertThat(receivedMessage).isNotNull(); + assertThat(receivedMessage.getUserId()).isEqualTo(sender.getId().toString()); + } + + @Test + @DisplayName("offer 테스트") + public void testOffer() throws InterruptedException, ExecutionException, TimeoutException { + BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + + SdpMessageDto sdpMessageDto = new SdpMessageDto(); + sdpMessageDto.setUserId(receiver.getId().toString()); + sdpMessageDto.setType(ScreenShare); + SdpDto sdpDto = new SdpDto(); + sdpDto.setSdp("sdp"); + sdpDto.setType("offer"); + sdpMessageDto.setSessionDescription(sdpDto); + + //유저 2명 연결 1이 송신 2가 수신. + StompSession senderSession = connectWebSocket(sender); + StompSession receiverSession = connectWebSocket(receiver); + receiverSession.subscribe("/user/queue/offer/123", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + System.out.println("구독 시작: /user/queue/offer/123"); + return SdpResponseDto.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + System.out.println("offer 응답값 : " + payload); + messageQueue.offer((SdpResponseDto)payload); + } + }); + + StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.setDestination(WEBSOCKET_TOPIC + "/offer/123"); + + senderSession.send(stompHeaders, sdpMessageDto); + + SdpResponseDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS); + assertThat(receivedMessage).isNotNull(); + assertThat(receivedMessage.getUserInfo().getUserId()).isEqualTo(sender.getId()); + } + + @Test + @DisplayName("소켓으로 연결된 유저에게 answer 메시지 전송할 수 있다.") + public void answerTest() throws InterruptedException, ExecutionException, TimeoutException { + BlockingQueue messageQueue = new LinkedBlockingQueue<>(); + + SdpMessageDto sdpMessageDto = new SdpMessageDto(); + SdpDto sdpDto = new SdpDto(); + sdpDto.setSdp("sdp"); + sdpDto.setType("answer"); + sdpMessageDto.setSessionDescription(sdpDto); + sdpMessageDto.setUserId(receiver.getId().toString()); + sdpMessageDto.setType(ScreenShare); + + //유저 2명 연결 1이 송신 2가 수신. + StompSession senderSession = connectWebSocket(sender); + StompSession receiverSession = connectWebSocket(receiver); + receiverSession.subscribe("/user/queue/answer/123", new StompFrameHandler() { + @Override + public Type getPayloadType(StompHeaders headers) { + System.out.println("구독 시작: /user/queue/answer/123"); + return SdpResponseDto.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + System.out.println("ice 응답값 : " + payload); + messageQueue.offer((SdpResponseDto)payload); + } + }); + StompHeaders stompHeaders = new StompHeaders(); + stompHeaders.setDestination(WEBSOCKET_TOPIC + "/answer/123"); + + senderSession.send(stompHeaders, sdpMessageDto); + + SdpResponseDto receivedMessage = messageQueue.poll(5, TimeUnit.SECONDS); + assertThat(receivedMessage).isNotNull(); + // assertThat(receivedMessage.getUserId()).isEqualTo(sender.getId().toString()); + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..fcff3dc --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,48 @@ +spring: + datasource: + url: jdbc:h2:mem:test + driverClassName: org.h2.Driver + username: sa + password: + + jpa: + hibernate: + ddl-auto: create + +oauth2: + jwt: + authorityKey: "roles" + bearerPrefix: "Bearer " + tokenSecret: "testSecretKeyShouldBeKeptSecretAndHasAtLeast32Bytes" + accessTokenHeader: "Authorization" + access-token-expire-day: 30 + refresh-token-expire-day: 30 + naver: + client-id: "test-client-id" + client-secret: "test-client-secret" + redirectUri: "http://localhost:8080/login/oauth2/code/naver" + + base-url: "https://nid.naver.com" + token-path: "/oauth2.0/token" + user-info-path: "/oauth2.0/profile" + authorizationUri: "https://nid.naver.com/oauth2.0/authorize" + tokenUri: "https://nid.naver.com/oauth2.0/token" + userInfoUri: "https://openapi.naver.com/v1/nid/me" + userNameAttributeName: response + + kakao: + name: Kakao + client-id: "test-client-id" + client-secret: "test-client-secret" + redirectUri: "http://localhost:8080/login/kakao/index.html" + authorizationUri: "https://kauth.kakao.com/oauth/authorize" + tokenUri: "https://kauth.kakao.com/oauth/token" + userInfoUri: "https://kapi.kakao.com/v2/user/me" + userNameAttributeName: id + +school: + info: + url: https://www.career.go.kr/cnet/openapi/getOpenApi + api: + key: + test-api-key \ No newline at end of file