diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java index 45e59df1..02ab979b 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java @@ -71,6 +71,6 @@ public ResponseEntity> getGraph( "ID: " + dashboardId + " 의 React-flow 데이터를 조회했습니다.", BodyForReactFlow.from(graph) )); - } + } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java new file mode 100644 index 00000000..4bfaa446 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ReqBodyForLiveblocksAuth.java @@ -0,0 +1,15 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.dto; + +import java.util.List; +import java.util.Map; + +public record ReqBodyForLiveblocksAuth( + String userId, + UserInfo userInfo, + Map>permissions +) { + public record UserInfo( + String name, + String avatar + ) {} +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java new file mode 100644 index 00000000..c7e29e22 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/ResBodyForAuthToken.java @@ -0,0 +1,5 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.dto; + +public record ResBodyForAuthToken( + String token +){ } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java index 4771b6f4..46cabef7 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java @@ -10,16 +10,24 @@ import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.ReqBodyForLiveblocksAuth; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node; import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.member.entity.Member; +import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; +import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import java.nio.file.AccessDeniedException; +import java.util.Collections; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -30,7 +38,8 @@ public class DashboardService { private final ObjectMapper objectMapper; private final SignatureService signatureService; private final RabbitTemplate rabbitTemplate; - + private final SpaceService spaceService; + private final LiveblocksClient liveblocksClient; // =========================== Graph 관련 메서드 =========================== @@ -134,8 +143,59 @@ public void queueGraphUpdate(Integer dashboardId, String requestBody, String sig rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); } + // =========================== 기타 메서드 =========================== + + /** + * 특정 스페이스에 대한 Liveblocks 접속 토큰(JWT)을 발급합니다. + * @param spaceId 스페이스 ID + * @param member 토큰을 요청하는 멤버 + * @return 발급된 JWT 문자열 + * @throws AccessDeniedException 멤버가 해당 스페이스에 속해있지 않거나 권한이 없는 경우 + */ + @Transactional(readOnly = true) + public String getAuthTokenForSpace(Integer spaceId, Member member) throws AccessDeniedException { + Space space = spaceService.findById(spaceId); + + // 해당 스페이스에 멤버가 속해있는지, PENDING 상태는 아닌지 확인 + Membership membership = membershipService.findByMemberAndSpace(member, space); + if (membership.getAuthority().equals(Authority.PENDING)) { + throw new AccessDeniedException("스페이스에 가입된 멤버가 아닙니다."); + } + // Liveblocks Room ID 생성 + String roomId = "space_" + space.getId(); + + // Liveblocks에 전달할 사용자 정보 생성 + String userId = String.valueOf(member.getId()); + ReqBodyForLiveblocksAuth.UserInfo userInfo = new ReqBodyForLiveblocksAuth.UserInfo( + member.getName(), + member.getProfileImageUrl() + ); + + // Liveblocks 권한 설정 (내 서비스의 Authority -> Liveblocks 권한으로 변환) + List permissions; + switch (membership.getAuthority()) { + case ADMIN, READ_WRITE: + permissions = List.of("room:write"); + break; + case READ_ONLY: + permissions = Collections.emptyList(); // 빈 리스트는 읽기 전용을 의미 + break; + default: + // PENDING 등 다른 상태는 위에서 이미 필터링됨 + throw new AccessDeniedException("유효하지 않은 권한입니다."); + } + // Liveblocks Client에 전달할 요청 객체 생성 + ReqBodyForLiveblocksAuth authRequest = new ReqBodyForLiveblocksAuth( + userId, + userInfo, + Map.of(roomId, permissions) + ); + + // LiveblocksClient를 통해 토큰 발급 요청 + return liveblocksClient.getAuthToken(authRequest); + } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java index 348579b6..2032b94c 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceController.java @@ -8,9 +8,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.ResBodyForAuthToken; +import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.space.membership.dto.etc.SpaceMemberInfo; import org.tuna.zoopzoop.backend.domain.space.membership.entity.Membership; @@ -38,6 +42,7 @@ public class ApiV1SpaceController { private final SpaceService spaceService; private final MembershipService membershipService; + private final DashboardService dashboardService; @PostMapping @Operation(summary = "스페이스 생성") @@ -206,5 +211,30 @@ public RsData getSpace( ); } + /** + * Liveblocks 접속을 위한 인증 토큰(JWT) 발급 API + * @param spaceId 스페이스 ID + * @param userDetails 현재 로그인한 사용자 정보 + * @return ResponseEntity> + */ + @PostMapping("/dashboard-auth/{spaceId}") + @Operation(summary = "Liveblocks 접속 토큰 발급") + public ResponseEntity> getAuthToken( + @PathVariable Integer spaceId, + @AuthenticationPrincipal CustomUserDetails userDetails) throws AccessDeniedException { + + Member member = userDetails.getMember(); + String token = dashboardService.getAuthTokenForSpace(spaceId, member); + + ResBodyForAuthToken response = new ResBodyForAuthToken(token); + + return ResponseEntity + .status(HttpStatus.OK) + .body(new RsData<>( + "200", + "Liveblocks 접속 토큰이 발급되었습니다.", + response + )); + } } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceService.java index cd59a982..842a6eb3 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceService.java @@ -14,6 +14,7 @@ import org.tuna.zoopzoop.backend.domain.space.space.exception.DuplicateSpaceNameException; import org.tuna.zoopzoop.backend.domain.space.space.repository.SpaceRepository; import org.tuna.zoopzoop.backend.global.aws.S3Service; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; @Service @RequiredArgsConstructor @@ -21,6 +22,7 @@ public class SpaceService { private final SpaceRepository spaceRepository; private final S3Service s3Service; private final MembershipService membershipService; + private final LiveblocksClient liveblocksClient; // ======================== 스페이스 조회 ======================== // @@ -57,17 +59,7 @@ public Space findByName(String name) { */ @Transactional public Space createSpace(@NotBlank @Length(max = 50) String name) { - Space newSpace = Space.builder() - .name(name) - .build(); - - try{ - return spaceRepository.save(newSpace); - }catch (DataIntegrityViolationException e) { - throw new DuplicateSpaceNameException("이미 존재하는 스페이스 이름입니다."); - } catch (Exception e) { - throw e; - } + return createSpace(name, null); } /** @@ -83,13 +75,19 @@ public Space createSpace(@NotBlank @Length(max = 50) String name, String thumbna .thumbnailUrl(thumbnailUrl) .build(); + Space savedSpace; try{ - return spaceRepository.save(newSpace); + savedSpace = spaceRepository.save(newSpace); }catch (DataIntegrityViolationException e) { throw new DuplicateSpaceNameException("이미 존재하는 스페이스 이름입니다."); } catch (Exception e) { throw e; } + + // Liveblocks에 방 생성 요청 + liveblocksClient.createRoom("space_" + savedSpace.getId()); + + return savedSpace; } /** @@ -104,6 +102,10 @@ public String deleteSpace(Integer spaceId) { Space space = spaceRepository.findById(spaceId) .orElseThrow(() -> new NoResultException("존재하지 않는 스페이스입니다.")); String spaceName = space.getName(); + String roomId = "space_" + space.getId(); + + // Liveblocks에 방 삭제 요청 + liveblocksClient.deleteRoom(roomId); spaceRepository.delete(space); diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/clients/liveblocks/LiveblocksClient.java b/src/main/java/org/tuna/zoopzoop/backend/global/clients/liveblocks/LiveblocksClient.java new file mode 100644 index 00000000..6942fcc8 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/global/clients/liveblocks/LiveblocksClient.java @@ -0,0 +1,113 @@ +package org.tuna.zoopzoop.backend.global.clients.liveblocks; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.ReqBodyForLiveblocksAuth; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.ResBodyForAuthToken; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LiveblocksClient { + + private final RestTemplate restTemplate; + + @Value("${liveblocks.secret-key}") + private String secretKey; + + private static final String LIVEBLOCKS_API_URL = "https://api.liveblocks.io/v2/rooms"; + private static final String AUTH_API_URL = "https://api.liveblocks.io/v2/authorize-user"; + /** + * Liveblocks 서버에 새로운 방을 생성합니다. + * @param roomId 생성할 방의 고유 ID (워크스페이스 ID와 동일하게 사용) + */ + public void createRoom(String roomId) { + // 1. HTTP 헤더 설정 (Authorization: Bearer sk_...) + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(secretKey); + headers.setContentType(MediaType.APPLICATION_JSON); + + // 2. Request Body 생성 (비공개 방으로 생성) + Map requestBody = new HashMap<>(); + requestBody.put("id", roomId); + requestBody.put("defaultAccesses", Collections.emptyList()); // 비공개(private) 방으로 설정 + + // 3. HTTP 요청 엔티티 생성 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + try { + // 4. Liveblocks API에 POST 요청 전송 + ResponseEntity response = restTemplate.postForEntity(LIVEBLOCKS_API_URL, requestEntity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info("Liveblocks room created successfully. roomId: {}", roomId); + } else { + log.error("Failed to create Liveblocks room. roomId: {}, status: {}, body: {}", + roomId, response.getStatusCode(), response.getBody()); + } + } catch (RestClientException e) { + log.error("Error while calling Liveblocks API to create room. roomId: {}", roomId, e); + // 필요하다면 여기서 커스텀 예외를 발생시켜 서비스 레이어에서 처리하도록 할 수 있습니다. + throw new RuntimeException("Liveblocks API call failed", e); + } + } + + /** + * Liveblocks 서버의 방을 삭제합니다. + * @param roomId 삭제할 방의 고유 ID + */ + public void deleteRoom(String roomId) { + String deleteUrl = LIVEBLOCKS_API_URL + "/" + roomId; + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(secretKey); + HttpEntity requestEntity = new HttpEntity<>(headers); + + try { + restTemplate.exchange(deleteUrl, HttpMethod.DELETE, requestEntity, Void.class); + log.info("Liveblocks room deleted successfully. roomId: {}", roomId); + } catch (RestClientException e) { + log.error("Error while calling Liveblocks API to delete room. roomId: {}", roomId, e); + // 방 삭제 실패가 전체 로직에 큰 영향을 주지 않는다면, + // 예외를 던지는 대신 에러 로그만 남기고 넘어갈 수도 있습니다. + // 여기서는 일단 예외를 던져서 트랜잭션을 롤백하도록 합니다. + throw new RuntimeException("Liveblocks API call failed", e); + } + } + + /** + * Liveblocks 사용자 인증 토큰(JWT)을 발급받습니다. + * @param request 인증에 필요한 사용자 정보, 권한 등을 담은 객체 + * @return 발급된 JWT 문자열 + */ + public String getAuthToken(ReqBodyForLiveblocksAuth request) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(secretKey); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + try { + ResponseEntity response = restTemplate.postForEntity(AUTH_API_URL, requestEntity, String.class); + if (response.getStatusCode().is2xxSuccessful()) { + log.info("Liveblocks auth token issued successfully for user: {}", request.userId()); + return response.getBody(); + } else { + log.error("Failed to issue Liveblocks auth token. user: {}, status: {}, body: {}", + request.userId(), response.getStatusCode(), response.getBody()); + throw new RuntimeException("Failed to issue Liveblocks auth token."); + } + } catch (RestClientException e) { + log.error("Error while calling Liveblocks auth API for user: {}", request.userId(), e); + throw new RuntimeException("Liveblocks auth API call failed", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/config/restTemplate/RestTemplateConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/config/restTemplate/RestTemplateConfig.java new file mode 100644 index 00000000..faba24cf --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/global/config/restTemplate/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package org.tuna.zoopzoop.backend.global.config.restTemplate; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index bc1aff58..fe816a90 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -2,6 +2,7 @@ import org.apache.commons.codec.binary.Hex; import org.junit.jupiter.api.*; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -10,6 +11,7 @@ import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; @@ -25,6 +27,7 @@ import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.repository.SpaceRepository; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; import javax.crypto.Mac; @@ -37,6 +40,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -56,6 +60,9 @@ class DashboardControllerTest extends ControllerTestSupport { @Autowired private TransactionTemplate transactionTemplate; + @MockitoBean + private LiveblocksClient liveblocksClient; + private Integer unauthorizedDashboardId; private Integer authorizedDashboardId; @@ -67,6 +74,9 @@ class DashboardControllerTest extends ControllerTestSupport { @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + // 1. 유저 생성 memberService.createMember("tester1_forDashboardControllerTest", "url", "dc1111", Provider.KAKAO); memberService.createMember("tester2_forDashboardControllerTest", "url", "dc2222", Provider.KAKAO); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index d598ddd8..44aa5c01 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -1,9 +1,11 @@ package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; import org.junit.jupiter.api.*; +import org.mockito.Mockito; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; @@ -12,6 +14,7 @@ import org.tuna.zoopzoop.backend.domain.dashboard.entity.Node; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; import java.util.Map; @@ -19,6 +22,7 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyString; @SpringBootTest @Transactional @@ -28,6 +32,9 @@ class GraphUpdateConsumerTest extends ControllerTestSupport { @Autowired private TransactionTemplate transactionTemplate; @Autowired private SpaceService spaceService; + @MockitoBean + private LiveblocksClient liveblocksClient; + // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) private Integer existingDashboardId; @@ -35,6 +42,9 @@ class GraphUpdateConsumerTest extends ControllerTestSupport { @BeforeAll void setUp(){ + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + spaceService.createSpace(existingSpaceName, "thumb1"); } diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java index 63ff5c95..043e4bb3 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/archive/controller/SpaceArchiveFolderControllerTest.java @@ -2,12 +2,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.archive.folder.dto.reqBodyForCreateFolder; @@ -25,6 +27,7 @@ import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; import java.time.LocalDate; @@ -32,6 +35,7 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -55,6 +59,9 @@ class SpaceArchiveFolderControllerTest { @Autowired private FolderRepository folderRepository; @Autowired private DataSourceRepository dataSourceRepository; + @MockitoBean + private LiveblocksClient liveblocksClient; + private static final String OWNER_PK = "sp1111"; private static final String READER_PK = "sp2222"; @@ -67,6 +74,9 @@ class SpaceArchiveFolderControllerTest { @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + // 사용자 생성 try { memberService.createMember("spaceOwner", "http://img/owner.png", OWNER_PK, Provider.KAKAO); } catch (Exception ignored) {} try { memberService.createMember("spaceReader", "http://img/reader.png", READER_PK, Provider.KAKAO); } catch (Exception ignored) {} diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteControllerTest.java index 2c16db5d..706aacaf 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1InviteControllerTest.java @@ -4,12 +4,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.Mockito; 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.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; @@ -18,8 +20,10 @@ import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -36,8 +40,14 @@ class ApiV1InviteControllerTest extends ControllerTestSupport { @Autowired private MembershipService membershipService; + @MockitoBean + private LiveblocksClient liveblocksClient; + @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + setUpMember(); setUpSpace(); setUpMembership(); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipControllerTest.java index 1d9ca3e9..5fae715d 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/controller/ApiV1MembershipControllerTest.java @@ -4,12 +4,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.Mockito; 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.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; @@ -17,8 +19,10 @@ import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -35,8 +39,14 @@ class ApiV1MembershipControllerTest extends ControllerTestSupport { @Autowired private MembershipService membershipService; + @MockitoBean + private LiveblocksClient liveblocksClient; + @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + setUpMember(); setUpSpace(); setUpMembership(); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java index 8d74ea52..c73ae29a 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java @@ -2,10 +2,12 @@ import jakarta.persistence.NoResultException; import org.junit.jupiter.api.*; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.member.entity.Member; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; @@ -15,12 +17,14 @@ import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; import org.tuna.zoopzoop.backend.domain.space.membership.repository.MembershipRepository; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import java.nio.file.AccessDeniedException; import java.util.List; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; @ActiveProfiles("test") @SpringBootTest @@ -38,8 +42,14 @@ class MembershipServiceTest { @Autowired private MemberRepository memberRepository; + @MockitoBean + private LiveblocksClient liveblocksClient; + @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + membershipRepository.deleteAll(); setUpMember(); setUpSpace(); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceControllerTest.java index eb3fb736..71e164c6 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/controller/ApiV1SpaceControllerTest.java @@ -1,12 +1,14 @@ package org.tuna.zoopzoop.backend.domain.space.space.controller; import org.junit.jupiter.api.*; +import org.mockito.Mockito; 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.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; @@ -15,10 +17,12 @@ import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; +import static org.mockito.ArgumentMatchers.anyString; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -36,8 +40,14 @@ class ApiV1SpaceControllerTest extends ControllerTestSupport { @Autowired private MembershipService membershipService; + @MockitoBean + private LiveblocksClient liveblocksClient; + @BeforeAll void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + setUpMember(); setUpSpace(); setUpMembership(); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceServiceTest.java index 5b05b541..45da246d 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/space/service/SpaceServiceTest.java @@ -5,14 +5,18 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; 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.bean.override.mockito.MockitoBean; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.exception.DuplicateSpaceNameException; +import org.tuna.zoopzoop.backend.global.clients.liveblocks.LiveblocksClient; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; @ActiveProfiles("test") @SpringBootTest @@ -21,8 +25,12 @@ class SpaceServiceTest { @Autowired private SpaceService spaceService; + @MockitoBean + private LiveblocksClient liveblocksClient; + @BeforeEach void setUp() { + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); spaceService.createSpace("기존 스페이스 1_forSpaceServiceTest"); spaceService.createSpace("기존 스페이스 2_forSpaceServiceTest"); } @@ -34,6 +42,9 @@ void createSpace_Success() { // Given String spaceName = "테스트 스페이스"; + // liveblocksClient.createRoom() 메서드가 호출될 때 실제로 아무 작업도 하지 않도록 설정 + Mockito.doNothing().when(liveblocksClient).createRoom(anyString()); + // When var createdSpace = spaceService.createSpace(spaceName); @@ -74,6 +85,9 @@ void deleteSpace_Success() { Integer spaceId = space.getId(); String spaceName = space.getName(); + // Mockito 설정: deleteRoom 메소드가 어떤 문자열로 호출되더라도 아무런 동작을 하지 않도록 설정 + Mockito.doNothing().when(liveblocksClient).deleteRoom(anyString()); + // When String deletedSpaceName = spaceService.deleteSpace(spaceId);