1+ package com .somemore .auth .jwt .service ;
2+
3+ import com .somemore .IntegrationTestSupport ;
4+ import com .somemore .auth .jwt .domain .EncodedToken ;
5+ import com .somemore .auth .jwt .domain .TokenType ;
6+ import com .somemore .auth .jwt .domain .UserRole ;
7+ import com .somemore .auth .jwt .exception .JwtErrorType ;
8+ import com .somemore .auth .jwt .exception .JwtException ;
9+ import com .somemore .auth .jwt .refresh .domain .RefreshToken ;
10+ import com .somemore .auth .jwt .refresh .manager .RefreshTokenManager ;
11+ import com .somemore .auth .jwt .validator .JwtValidator ;
12+ import io .jsonwebtoken .Claims ;
13+ import io .jsonwebtoken .Jwts ;
14+ import org .junit .jupiter .api .DisplayName ;
15+ import org .junit .jupiter .api .Test ;
16+ import org .springframework .beans .factory .annotation .Autowired ;
17+ import org .springframework .mock .web .MockHttpServletResponse ;
18+
19+ import javax .crypto .SecretKey ;
20+ import java .time .Instant ;
21+ import java .util .Date ;
22+ import java .util .UUID ;
23+
24+ import static org .assertj .core .api .Assertions .*;
25+
26+
27+ class JwtServiceTest extends IntegrationTestSupport {
28+
29+ @ Autowired
30+ private JwtService jwtService ;
31+ @ Autowired
32+ private JwtValidator jwtValidator ;
33+ @ Autowired
34+ private SecretKey secretKey ;
35+ @ Autowired
36+ private RefreshTokenManager refreshTokenManager ;
37+
38+ @ DisplayName ("토큰이 올바르게 생성된다" )
39+ @ Test
40+ void generateAndValidateToken () {
41+ // given
42+ String userId = UUID .randomUUID ().toString ();
43+ UserRole role = UserRole .VOLUNTEER ;
44+ TokenType tokenType = TokenType .ACCESS ;
45+
46+ // when
47+ EncodedToken token = jwtService .generateToken (userId , role .name (), tokenType );
48+
49+ // then
50+ Claims claims = jwtService .getClaims (token );
51+ assertThat (claims .get ("id" , String .class )).isEqualTo (userId );
52+ assertThat (claims .get ("role" , String .class )).isEqualTo (role .name ());
53+ assertThat (claims .getExpiration ()).isNotNull ();
54+ }
55+
56+ @ DisplayName ("토큰 만료 기간이 정확히 설정되어야 한다" )
57+ @ Test
58+ void tokenExpirationPeriodIsExact () {
59+ // given
60+ String userId = UUID .randomUUID ().toString ();
61+ UserRole role = UserRole .VOLUNTEER ;
62+
63+ // when
64+ EncodedToken accessToken = jwtService .generateToken (userId , role .name (), TokenType .ACCESS );
65+ EncodedToken refreshToken = jwtService .generateToken (userId , role .name (), TokenType .REFRESH );
66+
67+ // then
68+ Claims accessClaims = jwtService .getClaims (accessToken );
69+ Claims refreshClaims = jwtService .getClaims (refreshToken );
70+
71+ long accessTokenDuration = accessClaims .getExpiration ().getTime () - accessClaims .getIssuedAt ().getTime ();
72+ long refreshTokenDuration = refreshClaims .getExpiration ().getTime () - refreshClaims .getIssuedAt ().getTime ();
73+
74+ assertThat (accessTokenDuration ).isEqualTo (TokenType .ACCESS .getPeriod ());
75+ assertThat (refreshTokenDuration ).isEqualTo (TokenType .REFRESH .getPeriod ());
76+ }
77+
78+ @ DisplayName ("동일한 사용자로 여러 토큰 생성 시 서로 다른 값이어야 한다" )
79+ @ Test
80+ void multipleTokensForSameUserAreDifferent () {
81+ // given
82+ String userId = UUID .randomUUID ().toString ();
83+ UserRole role = UserRole .VOLUNTEER ;
84+
85+ // when
86+ EncodedToken token1 = jwtService .generateToken (userId , role .name (), TokenType .ACCESS );
87+ EncodedToken token2 = jwtService .generateToken (userId , role .name (), TokenType .ACCESS );
88+
89+ // then
90+ assertThat (token1 .value ()).isNotEqualTo (token2 .value ());
91+ }
92+
93+ @ DisplayName ("만료된 엑세스 토큰은 리프레시 토큰이 유효하다면 갱신된다" )
94+ @ Test
95+ void verifyAndRefreshExpiredToken () {
96+ // given
97+ String userId = UUID .randomUUID ().toString ();
98+ UserRole role = UserRole .VOLUNTEER ;
99+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
100+ createAndSaveRefreshToken (userId , expiredAccessToken , Instant .now ().plusMillis (TokenType .REFRESH .getPeriod ()));
101+
102+ MockHttpServletResponse mockResponse = new MockHttpServletResponse ();
103+
104+ // when
105+ jwtService .processAccessToken (expiredAccessToken , mockResponse );
106+
107+ // then
108+ assertRefreshedAccessToken (mockResponse );
109+ }
110+
111+ @ DisplayName ("만료된 엑세스 토큰은 리프레시 토큰이 유효하지 않다면 갱신되지 않고 예외가 발생한다" )
112+ @ Test
113+ void throwExceptionWhenRefreshTokenIsInvalid () {
114+ // given
115+ String userId = UUID .randomUUID ().toString ();
116+ UserRole role = UserRole .VOLUNTEER ;
117+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
118+
119+ EncodedToken expiredRefreshToken = createExpiredToken (userId , role );
120+ RefreshToken refreshToken = new RefreshToken (userId , expiredAccessToken , expiredRefreshToken );
121+ refreshTokenManager .save (refreshToken );
122+
123+ // when & then
124+ MockHttpServletResponse mockResponse = new MockHttpServletResponse ();
125+
126+ assertThatThrownBy (() -> jwtService .processAccessToken (expiredAccessToken , mockResponse ))
127+ .isInstanceOf (JwtException .class )
128+ .hasMessage (JwtErrorType .EXPIRED_TOKEN .getMessage ());
129+ }
130+
131+ @ DisplayName ("만료된 엑세스 토큰은 리프레시 토큰이 존재하지 않는다면 갱신되지 않고 예외가 발생한다" )
132+ @ Test
133+ void throwExceptionWhenRefreshTokenIsMissing () {
134+ // given
135+ String userId = UUID .randomUUID ().toString ();
136+ UserRole role = UserRole .VOLUNTEER ;
137+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
138+
139+ // when & then
140+ MockHttpServletResponse mockResponse = new MockHttpServletResponse ();
141+
142+ assertThatThrownBy (() -> jwtService .processAccessToken (expiredAccessToken , mockResponse ))
143+ .isInstanceOf (JwtException .class )
144+ .hasMessage (JwtErrorType .EXPIRED_TOKEN .getMessage ());
145+ }
146+
147+ @ DisplayName ("리프레시된 AccessToken은 쿠키에 올바르게 저장된다" )
148+ @ Test
149+ void refreshedAccessTokenIsSetInCookie () {
150+ // given
151+ String userId = UUID .randomUUID ().toString ();
152+ UserRole role = UserRole .VOLUNTEER ;
153+
154+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
155+ createAndSaveRefreshToken (userId , expiredAccessToken , Instant .now ().plusMillis (TokenType .REFRESH .getPeriod ()));
156+
157+ MockHttpServletResponse mockResponse = new MockHttpServletResponse ();
158+
159+ // when
160+ jwtService .processAccessToken (expiredAccessToken , mockResponse );
161+
162+ // then
163+ String cookieHeader = mockResponse .getHeader ("Set-Cookie" );
164+ assertThat (cookieHeader ).contains ("ACCESS=" );
165+ assertThat (cookieHeader ).contains ("HttpOnly" );
166+ assertThat (cookieHeader ).contains ("Secure" );
167+ }
168+
169+ @ DisplayName ("기존 RefreshToken이 갱신된다" )
170+ @ Test
171+ void refreshTokenIsUpdated () {
172+ // given
173+ String userId = UUID .randomUUID ().toString ();
174+ UserRole role = UserRole .VOLUNTEER ;
175+
176+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
177+ RefreshToken oldRefreshToken = createAndSaveRefreshToken (userId , expiredAccessToken , Instant .now ().plusMillis (TokenType .REFRESH .getPeriod ()));
178+
179+ EncodedToken newAccessToken = jwtService .generateToken (userId , role .name (), TokenType .ACCESS );
180+ RefreshToken newRefreshToken = createAndSaveRefreshToken (userId , newAccessToken , Instant .now ().plusMillis (TokenType .REFRESH .getPeriod ()));
181+
182+ // when
183+ // then
184+ assertThatThrownBy (() -> refreshTokenManager .findRefreshToken (expiredAccessToken ))
185+ .isInstanceOf (JwtException .class )
186+ .hasMessage (JwtErrorType .EXPIRED_TOKEN .getMessage ());
187+
188+ assertThat (newRefreshToken .getAccessToken ()).isEqualTo (newAccessToken .value ());
189+ assertThat (newRefreshToken .getRefreshToken ()).isNotEqualTo (oldRefreshToken .getRefreshToken ());
190+ }
191+
192+
193+ @ DisplayName ("잘못된 JWT 토큰은 예외가 발생한다" )
194+ @ Test
195+ void invalidTokenThrowsJwtException () {
196+ // given
197+ String invalidToken = "invalid.token.value" ;
198+ EncodedToken encodedToken = new EncodedToken (invalidToken );
199+
200+ // when
201+ // then
202+ assertThatThrownBy (() -> jwtValidator .validateToken (encodedToken ))
203+ .isInstanceOf (JwtException .class )
204+ .hasMessage (JwtErrorType .UNKNOWN_ERROR .getMessage ());
205+ }
206+
207+ @ DisplayName ("만료된 JWT 토큰은 예외가 발생한다" )
208+ @ Test
209+ void expiredTokenThrowsJwtException () {
210+ // given
211+ String userId = UUID .randomUUID ().toString ();
212+ UserRole role = UserRole .VOLUNTEER ;
213+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
214+
215+ // when
216+ // then
217+ assertThatThrownBy (() -> jwtValidator .validateToken (expiredAccessToken ))
218+ .isInstanceOf (JwtException .class )
219+ .hasMessage (JwtErrorType .EXPIRED_TOKEN .getMessage ());
220+ }
221+
222+ @ DisplayName ("RefreshToken이 존재하지 않으면 예외가 발생한다" )
223+ @ Test
224+ void refreshTokenNotFoundThrowsJwtException () {
225+ // given
226+ String userId = UUID .randomUUID ().toString ();
227+ UserRole role = UserRole .VOLUNTEER ;
228+ EncodedToken expiredAccessToken = createExpiredToken (userId , role );
229+
230+ // when
231+ // then
232+ assertThatThrownBy (() -> jwtService .processAccessToken (expiredAccessToken , new MockHttpServletResponse ()))
233+ .isInstanceOf (JwtException .class )
234+ .hasMessage (JwtErrorType .EXPIRED_TOKEN .getMessage ());
235+ }
236+
237+ private EncodedToken createExpiredToken (String userId , UserRole role ) {
238+ Claims claims = buildClaims (userId , role );
239+
240+ Instant now = Instant .now ();
241+ Instant expiration = now .plusMillis (-1 ); // 과거
242+
243+ return new EncodedToken (Jwts .builder ()
244+ .claims (claims )
245+ .issuedAt (Date .from (now ))
246+ .expiration (Date .from (expiration ))
247+ .signWith (secretKey , Jwts .SIG .HS256 )
248+ .compact ());
249+ }
250+
251+ private RefreshToken createAndSaveRefreshToken (String userId , EncodedToken accessToken , Instant expiration ) {
252+ Claims claims = buildClaims (userId , UserRole .VOLUNTEER );
253+ Instant now = Instant .now ();
254+ Instant refreshExpiration = now .plusMillis (TokenType .REFRESH .getPeriod ());
255+ String uniqueId = UUID .randomUUID ().toString (); // jti
256+
257+ RefreshToken refreshToken = new RefreshToken (
258+ userId ,
259+ accessToken ,
260+ new EncodedToken (Jwts .builder ()
261+ .claims (claims )
262+ .id (uniqueId )
263+ .issuedAt (Date .from (now ))
264+ .expiration (Date .from (refreshExpiration ))
265+ .signWith (secretKey , Jwts .SIG .HS256 )
266+ .compact ()));
267+
268+ refreshTokenManager .save (refreshToken );
269+
270+ return refreshToken ;
271+ }
272+
273+ private Claims buildClaims (String userId , UserRole role ) {
274+ return Jwts .claims ()
275+ .add ("id" , userId )
276+ .add ("role" , role )
277+ .build ();
278+ }
279+
280+ private void assertRefreshedAccessToken (MockHttpServletResponse mockResponse ) {
281+ String cookie = mockResponse .getHeader ("Set-Cookie" );
282+ assertThat (cookie ).isNotNull ();
283+ assertThat (cookie ).contains (TokenType .ACCESS .name ());
284+
285+ EncodedToken refreshedAccessToken = new EncodedToken (
286+ cookie .split (";" )[0 ].substring ("ACCESS=" .length ()));
287+ assertThatCode (() -> jwtValidator .validateToken (refreshedAccessToken ))
288+ .doesNotThrowAnyException ();
289+ }
290+
291+ }
0 commit comments