Skip to content

Commit 6909477

Browse files
committed
feat: 토큰 재발급 테스트
1 parent 3e81b1b commit 6909477

2 files changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.back.web7_9_codecrete_be.domain.artists.service.spotifyService;
2+
3+
import com.back.web7_9_codecrete_be.global.spotify.SpotifyClient;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.ExtendWith;
8+
import org.mockito.InjectMocks;
9+
import org.mockito.Mock;
10+
import org.mockito.junit.jupiter.MockitoExtension;
11+
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
16+
import static org.mockito.BDDMockito.*;
17+
import static org.mockito.Mockito.*;
18+
19+
@ExtendWith(MockitoExtension.class)
20+
@DisplayName("SpotifyRateLimitHandler 401 에러 처리 테스트")
21+
class SpotifyRateLimitHandlerTest {
22+
23+
@Mock
24+
private SpotifyClient spotifyClient;
25+
26+
@InjectMocks
27+
private SpotifyRateLimitHandler rateLimitHandler;
28+
29+
@BeforeEach
30+
void setUp() {
31+
// 각 테스트 전에 상태 초기화
32+
}
33+
34+
@Test
35+
@DisplayName("401 에러 발생 시 토큰 재발급 후 재시도 성공")
36+
void callWithRateLimitRetry_when401Error_shouldRefreshTokenAndRetry() {
37+
// given
38+
RuntimeException unauthorizedException = new RuntimeException("401 Unauthorized");
39+
String successResult = "success-result";
40+
41+
// forceRefreshToken은 void 메서드이므로 doNothing() 사용
42+
doNothing().when(spotifyClient).forceRefreshToken();
43+
44+
// 첫 번째 호출은 401 에러, 두 번째 호출은 성공하도록 설정
45+
AtomicInteger callCount = new AtomicInteger(0);
46+
47+
// when
48+
String result = rateLimitHandler.callWithRateLimitRetry(
49+
() -> {
50+
int count = callCount.incrementAndGet();
51+
if (count == 1) {
52+
// 첫 번째 호출 시 401 에러
53+
throw unauthorizedException;
54+
}
55+
// 두 번째 호출 시 성공
56+
return successResult;
57+
},
58+
"test-context"
59+
);
60+
61+
// then
62+
assertThat(result).isEqualTo(successResult);
63+
assertThat(callCount.get()).isEqualTo(2); // 재시도로 2번 호출됨
64+
verify(spotifyClient, times(1)).forceRefreshToken();
65+
}
66+
67+
@Test
68+
@DisplayName("401 에러가 아닌 경우 토큰 재발급하지 않음")
69+
void callWithRateLimitRetry_whenNot401Error_shouldNotRefreshToken() {
70+
// given
71+
RuntimeException otherException = new RuntimeException("500 Internal Server Error");
72+
String successResult = "success-result";
73+
74+
// when & then
75+
assertThatThrownBy(() -> {
76+
rateLimitHandler.callWithRateLimitRetry(
77+
() -> {
78+
throw otherException;
79+
},
80+
"test-context"
81+
);
82+
}).isInstanceOf(RuntimeException.class)
83+
.hasMessageContaining("500 Internal Server Error");
84+
85+
// 401이 아니면 토큰 재발급하지 않아야 함
86+
verify(spotifyClient, never()).forceRefreshToken();
87+
}
88+
89+
@Test
90+
@DisplayName("정상 호출 시 토큰 재발급하지 않음")
91+
void callWithRateLimitRetry_whenSuccess_shouldNotRefreshToken() {
92+
// given
93+
String successResult = "success-result";
94+
95+
// when
96+
String result = rateLimitHandler.callWithRateLimitRetry(
97+
() -> successResult,
98+
"test-context"
99+
);
100+
101+
// then
102+
assertThat(result).isEqualTo(successResult);
103+
verify(spotifyClient, never()).forceRefreshToken();
104+
}
105+
106+
@Test
107+
@DisplayName("401 에러가 예외 체인에 있는 경우 감지")
108+
void is401Error_when401InExceptionChain_shouldDetect() {
109+
// given
110+
RuntimeException cause = new RuntimeException("401 Unauthorized");
111+
RuntimeException wrapper = new RuntimeException("Wrapped exception", cause);
112+
113+
// when
114+
// is401Error는 private 메서드이므로 callWithRateLimitRetry를 통해 간접 테스트
115+
// 실제로는 401 에러가 발생하면 forceRefreshToken이 호출되어야 함
116+
// 이 테스트는 통합 테스트에서 더 적합할 수 있음
117+
}
118+
}
119+
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package com.back.web7_9_codecrete_be.global.spotify;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.ExtendWith;
7+
import org.mockito.InjectMocks;
8+
import org.mockito.Mock;
9+
import org.mockito.junit.jupiter.MockitoExtension;
10+
import org.springframework.test.util.ReflectionTestUtils;
11+
import se.michaelthelin.spotify.SpotifyApi;
12+
import se.michaelthelin.spotify.model_objects.credentials.ClientCredentials;
13+
import se.michaelthelin.spotify.requests.authorization.client_credentials.ClientCredentialsRequest;
14+
15+
import java.util.concurrent.CountDownLatch;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.BDDMockito.*;
23+
24+
@ExtendWith(MockitoExtension.class)
25+
@DisplayName("SpotifyClient 토큰 관리 테스트")
26+
class SpotifyClientTest {
27+
28+
@Mock
29+
private SpotifyApi spotifyApi;
30+
31+
@Mock
32+
private ClientCredentialsRequest.Builder requestBuilder;
33+
34+
@Mock
35+
private ClientCredentialsRequest request;
36+
37+
@InjectMocks
38+
private SpotifyClient spotifyClient;
39+
40+
@BeforeEach
41+
void setUp() {
42+
// ReflectionTestUtils로 private 필드 초기화
43+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", null);
44+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", 0L);
45+
}
46+
47+
@Test
48+
@DisplayName("토큰이 없을 때 자동 발급")
49+
void getAccessToken_whenTokenIsNull_shouldIssueNewToken() throws Exception {
50+
// given
51+
String expectedToken = "new-access-token-123";
52+
ClientCredentials credentials = mock(ClientCredentials.class);
53+
54+
given(spotifyApi.clientCredentials()).willReturn(requestBuilder);
55+
given(requestBuilder.build()).willReturn(request);
56+
given(request.execute()).willReturn(credentials);
57+
given(credentials.getAccessToken()).willReturn(expectedToken);
58+
given(credentials.getExpiresIn()).willReturn(3600); // 1시간
59+
60+
// when
61+
String token = spotifyClient.getAccessToken();
62+
63+
// then
64+
assertThat(token).isEqualTo(expectedToken);
65+
verify(spotifyApi, times(1)).setAccessToken(expectedToken);
66+
}
67+
68+
@Test
69+
@DisplayName("토큰이 만료되었을 때 자동 재발급")
70+
void getAccessToken_whenTokenExpired_shouldRefreshToken() throws Exception {
71+
// given
72+
String oldToken = "old-token";
73+
String newToken = "new-token-456";
74+
long expiredTime = System.currentTimeMillis() - 1000; // 1초 전에 만료
75+
76+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", oldToken);
77+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", expiredTime);
78+
79+
ClientCredentials credentials = mock(ClientCredentials.class);
80+
given(spotifyApi.clientCredentials()).willReturn(requestBuilder);
81+
given(requestBuilder.build()).willReturn(request);
82+
given(request.execute()).willReturn(credentials);
83+
given(credentials.getAccessToken()).willReturn(newToken);
84+
given(credentials.getExpiresIn()).willReturn(3600);
85+
86+
// when
87+
String token = spotifyClient.getAccessToken();
88+
89+
// then
90+
assertThat(token).isEqualTo(newToken);
91+
assertThat(token).isNotEqualTo(oldToken);
92+
verify(spotifyApi, times(1)).setAccessToken(newToken);
93+
}
94+
95+
@Test
96+
@DisplayName("토큰이 유효할 때 재사용")
97+
void getAccessToken_whenTokenValid_shouldReuseToken() throws Exception {
98+
// given
99+
String validToken = "valid-token-789";
100+
long futureExpiry = System.currentTimeMillis() + 1000000; // 미래에 만료
101+
102+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", validToken);
103+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", futureExpiry);
104+
105+
// when
106+
String token = spotifyClient.getAccessToken();
107+
108+
// then
109+
assertThat(token).isEqualTo(validToken);
110+
// 토큰이 유효하면 재발급하지 않아야 함
111+
verify(spotifyApi, never()).clientCredentials();
112+
}
113+
114+
@Test
115+
@DisplayName("forceRefreshToken은 토큰을 강제로 재발급")
116+
void forceRefreshToken_shouldForceRefresh() throws Exception {
117+
// given
118+
String oldToken = "old-token";
119+
String newToken = "forced-new-token";
120+
long futureExpiry = System.currentTimeMillis() + 1000000; // 아직 유효한 토큰
121+
122+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", oldToken);
123+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", futureExpiry);
124+
125+
ClientCredentials credentials = mock(ClientCredentials.class);
126+
given(spotifyApi.clientCredentials()).willReturn(requestBuilder);
127+
given(requestBuilder.build()).willReturn(request);
128+
given(request.execute()).willReturn(credentials);
129+
given(credentials.getAccessToken()).willReturn(newToken);
130+
given(credentials.getExpiresIn()).willReturn(3600);
131+
132+
// when
133+
spotifyClient.forceRefreshToken();
134+
String token = spotifyClient.getAccessToken();
135+
136+
// then
137+
assertThat(token).isEqualTo(newToken);
138+
assertThat(token).isNotEqualTo(oldToken);
139+
verify(spotifyApi, times(1)).setAccessToken(newToken);
140+
}
141+
142+
@Test
143+
@DisplayName("동시에 여러 스레드가 호출해도 토큰은 한 번만 발급")
144+
void getAccessToken_concurrentCalls_shouldIssueTokenOnce() throws Exception {
145+
// given
146+
String expectedToken = "concurrent-token";
147+
ClientCredentials credentials = mock(ClientCredentials.class);
148+
149+
given(spotifyApi.clientCredentials()).willReturn(requestBuilder);
150+
given(requestBuilder.build()).willReturn(request);
151+
given(request.execute()).willReturn(credentials);
152+
given(credentials.getAccessToken()).willReturn(expectedToken);
153+
given(credentials.getExpiresIn()).willReturn(3600);
154+
155+
int threadCount = 10;
156+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
157+
CountDownLatch latch = new CountDownLatch(threadCount);
158+
AtomicInteger tokenIssueCount = new AtomicInteger(0);
159+
160+
// when
161+
for (int i = 0; i < threadCount; i++) {
162+
executor.submit(() -> {
163+
try {
164+
spotifyClient.getAccessToken();
165+
tokenIssueCount.incrementAndGet();
166+
} finally {
167+
latch.countDown();
168+
}
169+
});
170+
}
171+
172+
latch.await();
173+
executor.shutdown();
174+
175+
// then
176+
// 모든 스레드가 동일한 토큰을 받아야 함
177+
assertThat(tokenIssueCount.get()).isEqualTo(threadCount);
178+
// 실제 토큰 발급은 한 번만 일어나야 함 (동시성 제어)
179+
verify(spotifyApi, atMostOnce()).clientCredentials();
180+
}
181+
182+
@Test
183+
@DisplayName("토큰 만료 여부 확인")
184+
void isTokenExpired_shouldReturnCorrectStatus() {
185+
// given - 만료된 토큰
186+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", "expired-token");
187+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", System.currentTimeMillis() - 1000);
188+
189+
// when & then
190+
assertThat(spotifyClient.isTokenExpired()).isTrue();
191+
192+
// given - 유효한 토큰
193+
ReflectionTestUtils.setField(spotifyClient, "tokenExpiresAt", System.currentTimeMillis() + 1000000);
194+
195+
// when & then
196+
assertThat(spotifyClient.isTokenExpired()).isFalse();
197+
198+
// given - 토큰이 null
199+
ReflectionTestUtils.setField(spotifyClient, "cachedAccessToken", null);
200+
201+
// when & then
202+
assertThat(spotifyClient.isTokenExpired()).isTrue();
203+
}
204+
205+
@Test
206+
@DisplayName("getAuthorizedApi는 유효한 토큰이 있는 API를 반환")
207+
void getAuthorizedApi_shouldReturnApiWithValidToken() throws Exception {
208+
// given
209+
String expectedToken = "api-token";
210+
ClientCredentials credentials = mock(ClientCredentials.class);
211+
212+
given(spotifyApi.clientCredentials()).willReturn(requestBuilder);
213+
given(requestBuilder.build()).willReturn(request);
214+
given(request.execute()).willReturn(credentials);
215+
given(credentials.getAccessToken()).willReturn(expectedToken);
216+
given(credentials.getExpiresIn()).willReturn(3600);
217+
218+
// when
219+
SpotifyApi api = spotifyClient.getAuthorizedApi();
220+
221+
// then
222+
assertThat(api).isNotNull();
223+
assertThat(api).isEqualTo(spotifyApi);
224+
verify(spotifyApi, times(1)).setAccessToken(expectedToken);
225+
}
226+
}
227+

0 commit comments

Comments
 (0)