Skip to content

Commit c94d591

Browse files
feat: enforce authentication on sendChat() with three-layer guard
미인증 상태에서 sendChat() 호출 시 AuthenticationException으로 즉시 실패하도록 SOOPChatClient, SOOPConnection, WebSocketManager에 인증 검사를 추가. 읽기 전용(이벤트 수신)은 인증 없이 유지.
1 parent cd73332 commit c94d591

File tree

6 files changed

+116
-4
lines changed

6 files changed

+116
-4
lines changed

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ public class Example {
103103

104104
chat.connectToChat().join();
105105

106-
// 채팅 전송
107-
chat.sendChat("Hello!");
108-
109106
// 프로그램 실행 유지
110107
Thread.sleep(Long.MAX_VALUE);
111108
}
@@ -139,14 +136,33 @@ public class DirectExample {
139136
}
140137
```
141138

142-
### 인증 (선택사항)
139+
### 인증 (채팅 전송 시 필수)
140+
141+
읽기 전용(이벤트 수신)은 인증 없이 사용할 수 있지만, `sendChat()`으로 채팅을 전송하려면 반드시 인증이 필요합니다.
143142

144143
```java
145144
SoopClient client = new SoopClient();
146145

146+
// 1. 로그인
147147
AuthCookie cookie = client.auth.signIn("userId", "password").join();
148148
if (cookie.success()) {
149149
System.out.println("로그인 성공");
150+
151+
// 2. 인증된 설정으로 채팅 클라이언트 생성
152+
SOOPChatConfig config = new SOOPChatConfig.Builder()
153+
.bid("streamerId")
154+
.authCookie(cookie)
155+
.build();
156+
157+
SOOPChatClient chat = new SOOPChatClient(config);
158+
159+
chat.on(ChatEvent.CHAT_MESSAGE, (ChatMessageEvent e) -> {
160+
System.out.println(e.senderNickname() + ": " + e.message());
161+
});
162+
163+
// 3. 연결 후 채팅 전송
164+
chat.connectToChat().join();
165+
chat.sendChat("Hello!").join();
150166
}
151167
```
152168

lib/src/main/java/com/github/getcurrentthread/soopapi/client/SOOPChatClient.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.github.getcurrentthread.soopapi.event.EventEmitter;
1515
import com.github.getcurrentthread.soopapi.event.EventListener;
1616
import com.github.getcurrentthread.soopapi.event.model.BaseEvent;
17+
import com.github.getcurrentthread.soopapi.exception.AuthenticationException;
1718
import com.github.getcurrentthread.soopapi.exception.ConnectionException;
1819
import com.github.getcurrentthread.soopapi.util.SOOPChatUtils;
1920

@@ -98,6 +99,10 @@ public void connectToChattingBlocking() throws ConnectionException {
9899
}
99100

100101
public CompletableFuture<Void> sendChat(String message) {
102+
if (!config.isAuthenticated()) {
103+
return CompletableFuture.failedFuture(
104+
new AuthenticationException("인증이 필요합니다. 채팅을 전송하려면 AuthCookie를 설정하세요."));
105+
}
101106
if (connection == null || !isConnected) {
102107
return CompletableFuture.failedFuture(
103108
new IllegalStateException("연결이 되어 있지 않습니다. 먼저 connectToChat을 호출하세요."));

lib/src/main/java/com/github/getcurrentthread/soopapi/connection/SOOPConnection.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.github.getcurrentthread.soopapi.event.ChatEvent;
1111
import com.github.getcurrentthread.soopapi.event.EventEmitter;
1212
import com.github.getcurrentthread.soopapi.event.model.JoinChannelEvent;
13+
import com.github.getcurrentthread.soopapi.exception.AuthenticationException;
1314
import com.github.getcurrentthread.soopapi.exception.ConnectionException;
1415
import com.github.getcurrentthread.soopapi.model.ChannelInfo;
1516
import com.github.getcurrentthread.soopapi.util.SOOPChatUtils;
@@ -161,6 +162,10 @@ public CompletableFuture<Void> reconnect() {
161162
}
162163

163164
public CompletableFuture<Void> sendChat(String message) {
165+
if (!config.isAuthenticated()) {
166+
return CompletableFuture.failedFuture(
167+
new AuthenticationException("인증이 필요합니다. 채팅을 전송하려면 AuthCookie를 설정하세요."));
168+
}
164169
return webSocketManager.sendChat(message);
165170
}
166171

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.getcurrentthread.soopapi.exception;
2+
3+
/** 인증이 필요한 작업을 미인증 상태에서 시도할 때 발생하는 예외입니다. */
4+
public class AuthenticationException extends SOOPChatException {
5+
6+
/**
7+
* 지정된 메시지로 새 AuthenticationException을 구성합니다.
8+
*
9+
* @param message 예외 메시지
10+
*/
11+
public AuthenticationException(String message) {
12+
super(message);
13+
}
14+
15+
/**
16+
* 지정된 메시지와 원인으로 새 AuthenticationException을 구성합니다.
17+
*
18+
* @param message 예외 메시지
19+
* @param cause 원인 (null 허용)
20+
*/
21+
public AuthenticationException(String message, Throwable cause) {
22+
super(message, cause);
23+
}
24+
}

lib/src/main/java/com/github/getcurrentthread/soopapi/websocket/WebSocketManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import javax.net.ssl.SSLContext;
1515

1616
import com.github.getcurrentthread.soopapi.config.SOOPChatConfig;
17+
import com.github.getcurrentthread.soopapi.exception.AuthenticationException;
1718
import com.github.getcurrentthread.soopapi.model.ChannelInfo;
1819
import com.github.getcurrentthread.soopapi.util.SSLContextProvider;
1920

@@ -356,6 +357,10 @@ public boolean isConnected() {
356357
}
357358

358359
public CompletableFuture<Void> sendChat(String message) {
360+
if (!config.isAuthenticated()) {
361+
return CompletableFuture.failedFuture(
362+
new AuthenticationException("인증이 필요합니다. 채팅을 전송하려면 AuthCookie를 설정하세요."));
363+
}
359364
if (!isConnected() || webSocket == null) {
360365
return CompletableFuture.failedFuture(
361366
new IllegalStateException("WebSocket is not connected"));

lib/src/test/java/com/github/getcurrentthread/soopapi/client/SOOPChatClientTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.github.getcurrentthread.soopapi.client;
22

3+
import static org.junit.jupiter.api.Assertions.*;
4+
35
import java.util.Arrays;
46
import java.util.concurrent.CountDownLatch;
7+
import java.util.concurrent.ExecutionException;
58
import java.util.concurrent.TimeUnit;
69
import java.util.logging.ConsoleHandler;
710
import java.util.logging.Level;
@@ -12,9 +15,11 @@
1215
import org.junit.jupiter.api.Tag;
1316
import org.junit.jupiter.api.Test;
1417

18+
import com.github.getcurrentthread.soopapi.api.model.AuthCookie;
1519
import com.github.getcurrentthread.soopapi.config.SOOPChatConfig;
1620
import com.github.getcurrentthread.soopapi.event.ChatEvent;
1721
import com.github.getcurrentthread.soopapi.event.model.ChatMessageEvent;
22+
import com.github.getcurrentthread.soopapi.exception.AuthenticationException;
1823
import com.github.getcurrentthread.soopapi.model.ChannelInfo;
1924
import com.github.getcurrentthread.soopapi.util.SOOPChatUtils;
2025

@@ -127,4 +132,56 @@ public void testSOOPChatClientConnection() throws Exception {
127132
LOGGER.info("Test completed.");
128133
}
129134
}
135+
136+
@Test
137+
void sendChat_withoutAuth_throwsAuthenticationException() {
138+
SOOPChatConfig config =
139+
new SOOPChatConfig.Builder().bid("testStreamer").bno("12345").build();
140+
141+
SOOPChatClient client = new SOOPChatClient(config);
142+
143+
ExecutionException ex =
144+
assertThrows(ExecutionException.class, () -> client.sendChat("Hello!").get());
145+
146+
assertInstanceOf(AuthenticationException.class, ex.getCause());
147+
}
148+
149+
@Test
150+
void sendChat_withFailedAuthCookie_throwsAuthenticationException() {
151+
AuthCookie failedCookie =
152+
new AuthCookie(
153+
"user", false, "", null, null, null, null, null, null, null, null, null,
154+
null);
155+
156+
SOOPChatConfig config =
157+
new SOOPChatConfig.Builder()
158+
.bid("testStreamer")
159+
.bno("12345")
160+
.authCookie(failedCookie)
161+
.build();
162+
163+
SOOPChatClient client = new SOOPChatClient(config);
164+
165+
ExecutionException ex =
166+
assertThrows(ExecutionException.class, () -> client.sendChat("Hello!").get());
167+
168+
assertInstanceOf(AuthenticationException.class, ex.getCause());
169+
}
170+
171+
@Test
172+
void sendChat_withoutAuth_prioritizesAuthOverConnection() {
173+
SOOPChatConfig config =
174+
new SOOPChatConfig.Builder().bid("testStreamer").bno("12345").build();
175+
176+
SOOPChatClient client = new SOOPChatClient(config);
177+
178+
// 미연결 + 미인증 상태에서 인증 오류가 먼저 발생해야 함
179+
assertFalse(client.isConnected());
180+
181+
ExecutionException ex =
182+
assertThrows(ExecutionException.class, () -> client.sendChat("Hello!").get());
183+
184+
assertInstanceOf(
185+
AuthenticationException.class, ex.getCause(), "연결 오류가 아닌 인증 오류가 먼저 발생해야 합니다");
186+
}
130187
}

0 commit comments

Comments
 (0)