Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, Message<?> message) {
throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER);
}

String email = authManager.resolveAccessToken(token);
String email = authManager.resolveChairmanToken(token);
return authService.getMember(email);
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.debatetimer.config.sharing;

import com.debatetimer.config.CorsProperties;
import java.time.Duration;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
Expand All @@ -15,6 +18,12 @@
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private static final long SERVER_TO_CLIENT_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long CLIENT_TO_SERVER_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final long SOCKJS_HEART_BEAT_DURATION = Duration.ofSeconds(10).toMillis();
private static final String HEART_BEAT_THREAD_PREFIX = "wss-heartbeat-";
private static final int HEART_BEAT_THREAD_COUNT = 1;

private final CorsProperties corsProperties;
private final WebSocketAuthMemberResolver webSocketAuthMemberResolver;

Expand All @@ -25,14 +34,26 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/room", "/chairman");
registry.enableSimpleBroker("/room", "/chairman")
.setHeartbeatValue(new long[]{SERVER_TO_CLIENT_HEARTBEAT_DURATION, CLIENT_TO_SERVER_HEARTBEAT_DURATION})
.setTaskScheduler(heartBeatScheduler());
registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns(corsProperties.getCorsOrigin())
.withSockJS();
.withSockJS()
.setHeartbeatTime(SOCKJS_HEART_BEAT_DURATION);
}

@Bean
public ThreadPoolTaskScheduler heartBeatScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(HEART_BEAT_THREAD_COUNT);
scheduler.setThreadNamePrefix(HEART_BEAT_THREAD_PREFIX);
scheduler.initialize();
return scheduler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.debatetimer.controller.sharing;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.response.ChairmanTokenResponse;
import com.debatetimer.service.sharing.SharingService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class SharingRestController {

private final SharingService sharingService;

@GetMapping("/api/share/{tableId}/chairman-token")
public ChairmanTokenResponse issueChairmanToken(
@AuthMember Member member,
@PathVariable("tableId") long tableId
) {
return sharingService.issueChairmanToken(tableId, member);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.debatetimer.controller.sharing;

import com.debatetimer.controller.auth.AuthMember;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.request.SharingRequest;
import com.debatetimer.dto.sharing.response.SharingResponse;
import com.debatetimer.service.sharing.SharingService;
Expand All @@ -13,13 +15,14 @@

@Controller
@RequiredArgsConstructor
public class SharingController {
public class SharingWebSocketController {

private final SharingService sharingService;

@MessageMapping("/event/{roomId}")
@SendTo("/room/{roomId}")
public SharingResponse share(
@AuthMember Member member,
@DestinationVariable(value = "roomId") long roomId,
@Valid @Payload SharingRequest request
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ public String resolveAccessToken(String accessToken) {
public String resolveRefreshToken(String refreshToken) {
return jwtTokenResolver.resolveRefreshToken(refreshToken);
}

public String resolveChairmanToken(String chairmanToken) {
return jwtTokenResolver.resolveChairmanToken(chairmanToken);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String createRefreshToken(MemberInfo memberInfo) {
return createToken(memberInfo, refreshTokenExpiration, TokenType.REFRESH_TOKEN);
}

public String createChairmanToken(MemberInfo memberInfo, long expirationSeconds) {
return createToken(memberInfo, Duration.ofSeconds(expirationSeconds), TokenType.CHAIRMAN_TOKEN);
}

private String createToken(MemberInfo memberInfo, Duration expiration, TokenType tokenType) {
Date now = new Date();
Date expiredDate = new Date(now.getTime() + expiration.toMillis());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public String resolveRefreshToken(String refreshToken) {
return resolveToken(refreshToken, TokenType.REFRESH_TOKEN);
}

public String resolveChairmanToken(String chairmanToken) {
return resolveToken(chairmanToken, TokenType.CHAIRMAN_TOKEN);
}

private String resolveToken(String token, TokenType tokenType) {
try {
Claims claims = Jwts.parserBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum TokenType {

ACCESS_TOKEN,
REFRESH_TOKEN
REFRESH_TOKEN,
CHAIRMAN_TOKEN
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public List<CustomizeTimeBox> getCustomizeTimeBoxes(long tableId, Member member)
return toCustomizeTimeBoxes(timeBoxEntityList, bellEntityList);
}

@Transactional(readOnly = true)
public long getTotalTimeBoxTimes(long tableId) {
return timeBoxRepository.sumTimeByTableId(tableId);
}

private List<CustomizeTimeBox> toCustomizeTimeBoxes(
List<CustomizeTimeBoxEntity> timeBoxEntities,
List<BellEntity> bellEntities
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.debatetimer.dto.sharing.response;

public record ChairmanTokenResponse(
String chairmanToken
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public interface CustomizeTimeBoxRepository extends Repository<CustomizeTimeBoxE

List<CustomizeTimeBoxEntity> findAllByCustomizeTable(CustomizeTableEntity table);

@Query("SELECT SUM(ctb.time) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
long sumTimeByTableId(long tableId);
Comment on lines +16 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

JPQL의 SUM 함수는 조회 결과가 없을 경우 null을 반환합니다. 현재 반환 타입이 Java 기본형인 long으로 되어 있어, null 반환 시 언박싱 과정에서 예외가 발생할 수 있습니다. COALESCE를 사용하여 결과가 없을 때 0을 반환하도록 처리하는 것이 안전합니다.

Suggested change
@Query("SELECT SUM(ctb.time) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
long sumTimeByTableId(long tableId);
@Query("SELECT COALESCE(SUM(ctb.time), 0) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
long sumTimeByTableId(long tableId);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

반영 예정

Comment on lines +16 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

JPQL/Hibernate에서 SUM(...)은 조건에 맞는 row가 0개일 때 null을 반환하나요? 공식 문서 또는 신뢰 가능한 레퍼런스 기준으로 확인해 주세요.

💡 Result:

네, JPQL/Hibernate에서 SUM(...) 집계 함수는 조건에 맞는 row가 0개일 때 null을 반환합니다. 이는 JPA 사양에 명시된 표준 동작입니다.

Citations:


🏁 Script executed:

# 파일 위치 확인
find . -name "CustomizeTimeBoxRepository.java" -type f

Repository: debate-timer/debate-timer-be

Length of output: 156


🏁 Script executed:

# sumTimeByTableId 메서드 호출처 검색
rg "sumTimeByTableId" --type java -B 2 -A 2

Repository: debate-timer/debate-timer-be

Length of output: 2045


🏁 Script executed:

# CustomizeTimeBoxRepository 파일 내용 확인
cat -n src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java

Repository: debate-timer/debate-timer-be

Length of output: 1190


🏁 Script executed:

# getTotalTimeBoxTimes 호출처 검색
rg "getTotalTimeBoxTimes" --type java -B 3 -A 3

Repository: debate-timer/debate-timer-be

Length of output: 1815


🏁 Script executed:

# 토큰 생성/발급 관련 로직과의 연결 확인
rg "token|Token" --type java -i | grep -i "customize\|TimeBox" | head -20

Repository: debate-timer/debate-timer-be

Length of output: 1979


🏁 Script executed:

# 테스트 코드 전체 확인 - empty 케이스 있는지 확인
cat -n src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java | head -80

Repository: debate-timer/debate-timer-be

Length of output: 4474


🏁 Script executed:

# sumTimeByTableId 테스트 부분 확인
rg "sumTimeByTableId" src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java -A 15 -B 5

Repository: debate-timer/debate-timer-be

Length of output: 598


🏁 Script executed:

# issueChairmanToken 메서드 전체 확인
cat -n src/main/java/com/debatetimer/service/sharing/SharingService.java | grep -A 10 "issueChairmanToken"

Repository: debate-timer/debate-timer-be

Length of output: 586


🏁 Script executed:

# empty 케이스 테스트 존재 여부 확인
rg "sumTimeByTableId|getTotalTimeBoxTimes" src/test -A 10 -B 5

Repository: debate-timer/debate-timer-be

Length of output: 1555


SUM 쿼리가 null을 반환할 때 primitive long 언박싱 예외 발생 위험

매칭 데이터가 없을 때 SUM(ctb.time)null을 반환하는데, 반환형이 primitive long이므로 NullPointerException이 발생합니다. 이 메서드는 SharingService.issueChairmanToken() (토큰 발급 API)에서 직접 호출되므로, 타임박스가 없는 테이블에서 토큰 발급 요청 시 500 에러로 이어집니다.

🔧 제안하는 수정
-@Query("SELECT SUM(ctb.time) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
+@Query("SELECT COALESCE(SUM(ctb.time), 0) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
 long sumTimeByTableId(long tableId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java`
around lines 16 - 17, The SUM query can return null and cause a primitive long
unboxing NPE in sumTimeByTableId; modify the repository query to return zero
instead of null by using COALESCE (e.g. change the JPQL in
CustomizeTimeBoxRepository.sumTimeByTableId to "SELECT COALESCE(SUM(ctb.time),
0) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId" so
the method can safely remain returning long), and then verify
SharingService.issueChairmanToken() uses that value directly.


@Query("DELETE FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId")
@Modifying(clearAutomatically = true, flushAutomatically = true)
void deleteAllByTable(long tableId);
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/debatetimer/service/sharing/SharingService.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
package com.debatetimer.service.sharing;

import com.debatetimer.controller.tool.jwt.JwtTokenProvider;
import com.debatetimer.domain.customize.CustomizeTable;
import com.debatetimer.domain.member.Member;
import com.debatetimer.domain.sharing.TimerEvent;
import com.debatetimer.domainrepository.customize.CustomizeTableDomainRepository;
import com.debatetimer.dto.member.MemberInfo;
import com.debatetimer.dto.sharing.request.SharingRequest;
import com.debatetimer.dto.sharing.response.ChairmanTokenResponse;
import com.debatetimer.dto.sharing.response.SharingResponse;
import com.debatetimer.dto.sharing.response.TimerEventDataResponse;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class SharingService {

private final JwtTokenProvider jwtTokenProvider;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

저희 이거 예전에 Oauth 로그인 할 때 AuthManager로 Controller 단에서 처리하기로 합의하지 않았었나요?

private final CustomizeTableDomainRepository customizeTableDomainRepository;

public SharingResponse share(SharingRequest request) {
TimerEvent timerEvent = request.toTimerEvent();
return Optional.ofNullable(timerEvent.getTimerEventData())
Expand All @@ -19,4 +30,11 @@ public SharingResponse share(SharingRequest request) {
))
.orElse(new SharingResponse(request.eventType(), null));
}

public ChairmanTokenResponse issueChairmanToken(long tableId, Member member) {
CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member);
long debateTime = customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId());
String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), debateTime * 2);
return new ChairmanTokenResponse(chairmanToken);
}
Comment on lines +34 to +39
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

타임박스가 없을 경우 토큰 만료 시간이 0이 됨

getTotalTimeBoxTimes가 0을 반환하면(타임박스가 없는 테이블) debateTime * 2 = 0이 되어 즉시 만료되는 토큰이 생성됩니다. 이는 의도된 동작이 아닐 수 있습니다.

최소 만료 시간을 설정하거나, 타임박스가 없는 경우에 대한 처리를 고려해 주세요.

🛡️ 최소 만료 시간 적용 제안
 public ChairmanTokenResponse issueChairmanToken(long tableId, Member member) {
+    private static final long MINIMUM_EXPIRATION_SECONDS = 300; // 최소 5분
+
     CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member);
     long debateTime = customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId());
-    String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), debateTime * 2);
+    long expirationSeconds = Math.max(debateTime * 2, MINIMUM_EXPIRATION_SECONDS);
+    String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), expirationSeconds);
     return new ChairmanTokenResponse(chairmanToken);
 }

또는 타임박스가 없는 테이블에 대해서는 예외를 발생시키는 방법도 있습니다:

if (debateTime == 0) {
    throw new DTClientErrorException(ClientErrorCode.NO_TIME_BOXES_CONFIGURED);
}
📝 Committable suggestion

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

Suggested change
public ChairmanTokenResponse issueChairmanToken(long tableId, Member member) {
CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member);
long debateTime = customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId());
String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), debateTime * 2);
return new ChairmanTokenResponse(chairmanToken);
}
private static final long MINIMUM_EXPIRATION_SECONDS = 300; // 최소 5분
public ChairmanTokenResponse issueChairmanToken(long tableId, Member member) {
CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member);
long debateTime = customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId());
long expirationSeconds = Math.max(debateTime * 2, MINIMUM_EXPIRATION_SECONDS);
String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), expirationSeconds);
return new ChairmanTokenResponse(chairmanToken);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/debatetimer/service/sharing/SharingService.java` around
lines 34 - 39, The method issueChairmanToken currently multiplies debateTime
from customizeTableDomainRepository.getTotalTimeBoxTimes(...) by 2 and may pass
0 to jwtTokenProvider.createChairmanToken, creating an immediately expired
token; update issueChairmanToken to handle debateTime == 0 by either enforcing a
minimum expiry (e.g., compute expiry = Math.max(debateTime * 2, MIN_EXPIRY_MS)
and pass expiry to jwtTokenProvider.createChairmanToken) or throw a
domain/client exception (e.g., DTClientErrorException with a
NO_TIME_BOXES_CONFIGURED code) so that ChairmanTokenResponse is not created with
a zero TTL; locate this logic in issueChairmanToken and adjust the flow
accordingly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

이미 타임박스가 없는 경우는 도메인적으로 제한하고 있어

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for domain-level restrictions on time boxes (e.g., validation, exceptions when empty)
rg -n "timeBox|TimeBox" --type java -C3 | grep -i "empty\|size\|isEmpty\|validate\|minimum\|least\|required\|exception\|error" | head -40

# Look at CustomizeTable domain class for constraints
fd "CustomizeTable.java" --exec cat {}

# Look for any validation related to time boxes being required
rg -n "NO_TIME\|EMPTY.*TIME\|TIME.*EMPTY\|timeBox.*size\|size.*timeBox" --type java -C3 | head -30

Length of output: 8190


🏁 Script executed:

#!/bin/bash
# Look for the request DTO that contains the table/timebox list with validation
fd "CustomizeTableCreateRequest.java\|CustomizeTableRequest.java\|TableCreateRequest.java" --exec cat {}

# Also search for `@NotEmpty` or `@Size` on table-related fields
rg -n "@NotEmpty|@Size|@Min" --type java -B2 -A2 | grep -A4 -B4 "table\|timeBox\|TimeBox" | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all request DTOs in the customize package
fd -e java . src/main/java --exec grep -l "table\|timeBox" {} | head -20

# Look for validation annotations in dto/request files
rg -n "NotEmpty\|Size\|NotNull" src/main/java --type java -B3 -A1 | head -80

# Find the customize request DTOs
fd -e java "Request" src/main/java/com/debatetimer/dto --exec cat {}

Length of output: 7461


🏁 Script executed:

#!/bin/bash
# Read CustomizeService to find validation logic for empty time boxes
fd "CustomizeService.java" --exec cat {}

# Also check if there's a domain class that validates the time box list
fd "CustomizeTimeBoxes.java\|CustomizeTableTimeBoxes.java" --exec cat {}

# Search for any `@NotEmpty` or `@Size`(min on table fields
rg -n "NotEmpty|Size\(min" --type java -B5 -A2 | head -60

Length of output: 2613


🏁 Script executed:

#!/bin/bash
# Read CustomizeTableDomainRepository for any validation on time boxes
fd "CustomizeTableDomainRepository.java" --exec cat {}

# Look for any validation in the domain entities or value objects about minimum timebox count
rg -n "empty\|isEmpty\|size.*0\|EMPTY_TABLE\|NO_TIMEBOX\|EMPTY_TIMEBOX" --type java -C3 | head -60

Length of output: 5082


@coli-geonwoo, 확인해주셔서 감사합니다. 도메인 레벨에서 이미 타임박스가 없는 테이블 생성을 제한하고 있다면, 해당 우려는 적용되지 않습니다.

다만 코드를 살펴보면서 한 가지 추가로 확인하실 사항이 있습니다. sumTimeByTableId의 JPQL 쿼리는 SUM(ctb.time)을 사용하는데, TimeBasedTimeBox 타입의 타임박스는 time 컬럼 대신 timePerTeam/timePerSpeaking을 사용합니다. 만약 테이블에 TimeBasedTimeBox 타입의 타임박스만 있을 경우 SUM 결과가 null이 되어 반환 타입인 primitive long에 할당 시 NullPointerException이 발생할 수 있습니다. 의도된 동작인지 확인해보시면 좋을 것 같습니다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coli-geonwoo 확인 해주세요

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.debatetimer.service.organization.OrganizationService;
import com.debatetimer.service.poll.PollService;
import com.debatetimer.service.poll.VoteService;
import com.debatetimer.service.sharing.SharingService;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.filter.log.RequestLoggingFilter;
Expand Down Expand Up @@ -71,6 +72,9 @@ public abstract class BaseDocumentTest {
@MockitoBean
protected VoteService voteService;

@MockitoBean
protected SharingService sharingService;

@MockitoBean
protected OrganizationService organizationService;

Expand Down
1 change: 1 addition & 0 deletions src/test/java/com/debatetimer/controller/Tag.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum Tag {
PARLIAMENTARY_API("Parliamentary Table API"),
TIME_BASED_API("Time Based Table API"),
CUSTOMIZE_API("Customize Table API"),
SHARING_API("Sharing API"),
POLL_API("Poll API"),
ORGANIZATION_API("Organization API");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.debatetimer.controller.sharing;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.payload.JsonFieldType.STRING;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;

import com.debatetimer.controller.BaseDocumentTest;
import com.debatetimer.controller.RestDocumentationRequest;
import com.debatetimer.controller.RestDocumentationResponse;
import com.debatetimer.controller.Tag;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.sharing.response.ChairmanTokenResponse;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;

public class SharingDocumentTest extends BaseDocumentTest {


@Nested
Comment on lines +22 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
public class SharingDocumentTest extends BaseDocumentTest {
@Nested
public class SharingDocumentTest extends BaseDocumentTest {
@Nested

class IssueChairmanToken {

private final RestDocumentationRequest requestDocument = request()
.tag(Tag.SHARING_API)
.summary("사회자용 토큰 발급")
.requestHeader(headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰"))
.pathParameter(parameterWithName("tableId").description("테이블 id"));

private final RestDocumentationResponse responseDocument = response()
.responseBodyField(
fieldWithPath("chairmanToken").type(STRING).description("사회자용 토큰")
);

@Test
void 사회자_용_토큰_생성_성공() {
long requestTableId = 1L;
ChairmanTokenResponse chairmanTokenResponse = new ChairmanTokenResponse("testToken");
doReturn(chairmanTokenResponse).when(sharingService)
.issueChairmanToken(eq(requestTableId), any(Member.class));

var document = document("sharing/get", 200)
.request(requestDocument)
.response(responseDocument)
.build();

given(document)
.contentType(ContentType.JSON)
.headers(EXIST_MEMBER_HEADER)
.pathParam("tableId", String.valueOf(requestTableId))
.when().get("/api/share/{tableId}/chairman-token")
.then().statusCode(200);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import org.junit.jupiter.params.provider.NullSource;
import org.springframework.messaging.simp.stomp.StompHeaders;

class SharingControllerTest extends BaseStompTest {
class SharingWebSocketControllerTest extends BaseStompTest {

@Nested
class Share {
Expand All @@ -31,7 +31,7 @@ class Share {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
Expand All @@ -58,12 +58,34 @@ class Share {
);
}

@Test
void 사회자가_아니면_이벤트를_발행할_수_없다() throws ExecutionException, InterruptedException, TimeoutException {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
CustomizeBoxType.NORMAL,
null,
2,
30
)
);
stompSession.subscribe("/room/" + roomId, handler); //청중의 구독
stompSession.send("/app/event/" + roomId, request); //사회자의 이벤트 발생

assertThatThrownBy(() -> handler.getCompletableFuture()
.get(2L, TimeUnit.SECONDS))
.isInstanceOf(TimeoutException.class);
}
Comment thread
coli-geonwoo marked this conversation as resolved.

@Test
void 사회자가_발생시킨_토론_종료_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(TimerEventType.FINISHED, null);
stompSession.subscribe("/room/" + roomId, handler); //청중의 구독

Expand All @@ -84,7 +106,7 @@ class Share {
long roomId = 1L;
MessageFrameHandler<SharingResponse> handler = new MessageFrameHandler<>(SharingResponse.class);
Member member = memberGenerator.generate("example@email.com");
StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member);
StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member);
SharingRequest request = new SharingRequest(
TimerEventType.NEXT,
new TimerEventInfoRequest(
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/com/debatetimer/fixture/HeaderGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public Headers generateAccessTokenHeader(Member member) {
return new Headers(new Header(HttpHeaders.AUTHORIZATION, accessToken));
}

public StompHeaders generateAccessTokenHeader(String destination, Member member) {
String accessToken = jwtTokenProvider.createAccessToken(new MemberInfo(member));
public StompHeaders generateChairmanTokenHeader(String destination, Member member) {
String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), 5L);
StompHeaders stompHeaders = new StompHeaders();
stompHeaders.setDestination(destination);
stompHeaders.add(HttpHeaders.AUTHORIZATION, accessToken);
stompHeaders.add(HttpHeaders.AUTHORIZATION, chairmanToken);
return stompHeaders;
}
}
Loading
Loading