Skip to content

Commit 3df7a32

Browse files
committed
test: SSENotificationV2ServiceImplTest 테스트코드 수정
1 parent 93dd609 commit 3df7a32

1 file changed

Lines changed: 105 additions & 29 deletions

File tree

src/test/java/store/lastdance/service/notification/SSENotificationV2ServiceImplTest.java

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,11 @@ class SSENotificationV2ServiceImplTest {
4141

4242
private SSENotificationV2ServiceImpl service;
4343

44-
// LocalDateTime 직렬화를 위해 JavaTimeModule 등록
4544
private final ObjectMapper objectMapper = new ObjectMapper()
4645
.registerModule(new JavaTimeModule());
4746

4847
@BeforeEach
4948
void setUp() {
50-
// SSENotificationV2ServiceImpl 생성자 직접 호출
51-
// @PostConstruct(init)은 private이라 수동 호출 불가 → addMessageListener는 mock이라 아무것도 안 함
5249
service = new SSENotificationV2ServiceImpl(
5350
onlineStatusService, redisTemplate, redisMessageListenerContainer, objectMapper, 2);
5451
}
@@ -65,22 +62,35 @@ class CreateConnection {
6562
void success_returnsEmitterAndSetsOnline() {
6663
UUID userId = UUID.randomUUID();
6764

68-
SseEmitter emitter = service.createConnection(userId);
65+
SseEmitter emitter = service.createConnection(userId, newConnectionId());
6966

7067
assertThat(emitter).isNotNull();
7168
then(onlineStatusService).should().setUserOnline(userId);
7269
}
7370

7471
@Test
75-
@DisplayName("같은 userId로 재연결하면 기존 연결과 다른 emitter를 반환한다")
76-
void reconnect_returnsDifferentEmitter() {
72+
@DisplayName("같은 userId로 두 번째 탭을 열어도 첫 번째 emitter가 살아있다")
73+
void multiTab_bothEmittersAlive() {
7774
UUID userId = UUID.randomUUID();
7875

79-
SseEmitter first = service.createConnection(userId);
80-
SseEmitter second = service.createConnection(userId);
76+
SseEmitter emitterA = service.createConnection(userId, newConnectionId());
77+
SseEmitter emitterB = service.createConnection(userId, newConnectionId());
8178

82-
assertThat(first).isNotSameAs(second);
83-
then(onlineStatusService).should(times(2)).setUserOnline(userId);
79+
assertThat(emitterA).isNotNull();
80+
assertThat(emitterB).isNotNull();
81+
assertThat(emitterA).isNotSameAs(emitterB);
82+
}
83+
84+
@Test
85+
@DisplayName("같은 userId로 두 탭이 연결돼도 setUserOnline은 최초 1회만 호출된다")
86+
void multiTab_setOnlineCalledOnce() {
87+
UUID userId = UUID.randomUUID();
88+
89+
service.createConnection(userId, newConnectionId());
90+
service.createConnection(userId, newConnectionId());
91+
92+
// 첫 번째 연결 시에만 온라인 전환
93+
then(onlineStatusService).should(times(1)).setUserOnline(userId);
8494
}
8595

8696
@Test
@@ -89,8 +99,8 @@ void differentUsers_independentEmitters() {
8999
UUID userA = UUID.randomUUID();
90100
UUID userB = UUID.randomUUID();
91101

92-
SseEmitter emitterA = service.createConnection(userA);
93-
SseEmitter emitterB = service.createConnection(userB);
102+
SseEmitter emitterA = service.createConnection(userA, newConnectionId());
103+
SseEmitter emitterB = service.createConnection(userB, newConnectionId());
94104

95105
assertThat(emitterA).isNotSameAs(emitterB);
96106
then(onlineStatusService).should().setUserOnline(userA);
@@ -109,7 +119,7 @@ class DisconnectUser {
109119
@DisplayName("연결된 사용자를 끊으면 오프라인 상태로 설정한다")
110120
void connected_setsOffline() {
111121
UUID userId = UUID.randomUUID();
112-
service.createConnection(userId);
122+
service.createConnection(userId, newConnectionId());
113123

114124
service.disconnectUser(userId);
115125

@@ -126,19 +136,77 @@ void notConnected_setsOfflineSafely() {
126136
then(onlineStatusService).should().setUserOffline(userId);
127137
}
128138

139+
@Test
140+
@DisplayName("멀티탭 상태에서 disconnectUser 호출 시 모든 탭이 끊기고 오프라인 전환된다")
141+
void multiTab_disconnectUser_allTabsClosed() {
142+
UUID userId = UUID.randomUUID();
143+
service.createConnection(userId, newConnectionId());
144+
service.createConnection(userId, newConnectionId());
145+
146+
service.disconnectUser(userId);
147+
148+
then(onlineStatusService).should().setUserOffline(userId);
149+
}
150+
129151
@Test
130152
@DisplayName("disconnect 후 재연결하면 새로운 emitter가 반환된다")
131153
void disconnectThenReconnect_returnsNewEmitter() {
132154
UUID userId = UUID.randomUUID();
133-
SseEmitter first = service.createConnection(userId);
155+
SseEmitter first = service.createConnection(userId, newConnectionId());
134156
service.disconnectUser(userId);
135157

136-
SseEmitter second = service.createConnection(userId);
158+
SseEmitter second = service.createConnection(userId, newConnectionId());
137159

138160
assertThat(first).isNotSameAs(second);
139161
}
140162
}
141163

164+
// ──────────────────────────────────────────────
165+
// disconnectConnection
166+
// ──────────────────────────────────────────────
167+
@Nested
168+
@DisplayName("disconnectConnection")
169+
class DisconnectConnection {
170+
171+
@Test
172+
@DisplayName("탭 하나만 끊으면 나머지 탭은 유지되고 온라인 상태가 유지된다")
173+
void oneTab_remainsOnline() {
174+
UUID userId = UUID.randomUUID();
175+
String connIdA = newConnectionId();
176+
String connIdB = newConnectionId();
177+
service.createConnection(userId, connIdA);
178+
service.createConnection(userId, connIdB);
179+
180+
service.disconnectConnection(userId, connIdA);
181+
182+
// 아직 탭B가 남아있으므로 오프라인 전환 없음
183+
then(onlineStatusService).should(never()).setUserOffline(userId);
184+
}
185+
186+
@Test
187+
@DisplayName("마지막 탭을 disconnectConnection으로 끊으면 오프라인으로 전환된다")
188+
void lastTab_setsOffline() {
189+
UUID userId = UUID.randomUUID();
190+
String connId = newConnectionId();
191+
service.createConnection(userId, connId);
192+
193+
service.disconnectConnection(userId, connId);
194+
195+
then(onlineStatusService).should().setUserOffline(userId);
196+
}
197+
198+
@Test
199+
@DisplayName("존재하지 않는 connectionId로 끊어도 예외가 발생하지 않는다")
200+
void unknownConnectionId_noException() {
201+
UUID userId = UUID.randomUUID();
202+
service.createConnection(userId, newConnectionId());
203+
204+
service.disconnectConnection(userId, "non-existent-id");
205+
206+
// 예외 없이 통과하면 성공
207+
}
208+
}
209+
142210
// ──────────────────────────────────────────────
143211
// sendNotification
144212
// ──────────────────────────────────────────────
@@ -197,18 +265,26 @@ class OnMessage {
197265
@DisplayName("연결된 사용자의 Redis 메시지는 예외 없이 처리된다")
198266
void connectedUser_processedWithoutException() throws Exception {
199267
UUID userId = UUID.randomUUID();
200-
service.createConnection(userId);
268+
service.createConnection(userId, newConnectionId());
201269

202270
service.onMessage(buildRedisMessage(userId, NotificationType.SCHEDULE), null);
271+
}
203272

204-
// emitter.send() 실패해도 바깥으로 예외 전파 없음 — 이 자체가 검증
273+
@Test
274+
@DisplayName("멀티탭 사용자에게 메시지가 오면 모든 탭에 전송 시도한다")
275+
void multiTab_messageDeliveredToAllTabs() throws Exception {
276+
UUID userId = UUID.randomUUID();
277+
service.createConnection(userId, newConnectionId());
278+
service.createConnection(userId, newConnectionId());
279+
280+
// 예외 없이 두 탭 모두에 전송 시도되면 성공
281+
service.onMessage(buildRedisMessage(userId, NotificationType.SCHEDULE), null);
205282
}
206283

207284
@Test
208285
@DisplayName("연결되지 않은 사용자의 메시지는 무시하고 오프라인 처리도 하지 않는다")
209286
void notConnectedUser_ignored() throws Exception {
210287
UUID userId = UUID.randomUUID();
211-
// createConnection 미호출 → userLocks에 없음
212288

213289
service.onMessage(buildRedisMessage(userId, NotificationType.PAYMENT), null);
214290

@@ -222,8 +298,6 @@ void malformedJson_swallowsException() {
222298
given(redisMessage.getBody()).willReturn("invalid-json".getBytes());
223299

224300
service.onMessage(redisMessage, null);
225-
226-
// 예외 없이 통과하면 성공
227301
}
228302
}
229303

@@ -245,12 +319,10 @@ void noConnections_noop() {
245319
@Test
246320
@DisplayName("연결이 있어도 예외 없이 완료된다")
247321
void withConnections_completesWithoutException() {
248-
service.createConnection(UUID.randomUUID());
249-
service.createConnection(UUID.randomUUID());
322+
service.createConnection(UUID.randomUUID(), newConnectionId());
323+
service.createConnection(UUID.randomUUID(), newConnectionId());
250324

251325
service.cleanupInactiveConnections();
252-
253-
// ping 전송은 테스트 환경에서 실패할 수 있으나 예외 전파 없어야 함
254326
}
255327
}
256328

@@ -268,20 +340,24 @@ void noConnections_shutdownCleanly() {
268340
}
269341

270342
@Test
271-
@DisplayName("연결이 있는 상태에서 shutdown하면 모든 연결을 정리한다")
272-
void withConnections_cleansAllUp() {
273-
service.createConnection(UUID.randomUUID());
274-
service.createConnection(UUID.randomUUID());
343+
@DisplayName("멀티탭 연결이 있는 상태에서 shutdown하면 모든 연결을 정리한다")
344+
void withMultiTabConnections_cleansAllUp() {
345+
UUID userId = UUID.randomUUID();
346+
service.createConnection(userId, newConnectionId());
347+
service.createConnection(userId, newConnectionId());
275348

276349
service.shutdown();
277-
// 종료 후 추가 연결 시도 없이 예외 없으면 성공
278350
}
279351
}
280352

281353
// ──────────────────────────────────────────────
282354
// helpers
283355
// ──────────────────────────────────────────────
284356

357+
private String newConnectionId() {
358+
return UUID.randomUUID().toString();
359+
}
360+
285361
private Message buildRedisMessage(UUID userId, NotificationType type) throws Exception {
286362
NotificationMessage payload = new NotificationMessage(userId, "제목", "내용", type, "relatedId");
287363
byte[] body = objectMapper.writeValueAsBytes(payload);

0 commit comments

Comments
 (0)