1+ package org .fontory .fontorybe .integration .authentication .adapter .inbound ;
2+
3+ import com .fasterxml .jackson .databind .ObjectMapper ;
4+ import jakarta .servlet .http .Cookie ;
5+ import org .fontory .fontorybe .authentication .application .AuthService ;
6+ import org .fontory .fontorybe .authentication .application .port .JwtTokenProvider ;
7+ import org .fontory .fontorybe .authentication .application .port .TokenStorage ;
8+ import org .fontory .fontorybe .authentication .domain .UserPrincipal ;
9+ import org .fontory .fontorybe .member .controller .port .MemberLookupService ;
10+ import org .fontory .fontorybe .member .domain .Member ;
11+ import org .junit .jupiter .api .BeforeEach ;
12+ import org .junit .jupiter .api .DisplayName ;
13+ import org .junit .jupiter .api .Test ;
14+ import org .springframework .beans .factory .annotation .Autowired ;
15+ import org .springframework .boot .test .autoconfigure .web .servlet .AutoConfigureMockMvc ;
16+ import org .springframework .boot .test .context .SpringBootTest ;
17+ import org .springframework .test .context .bean .override .mockito .MockitoBean ;
18+ import org .springframework .test .context .jdbc .Sql ;
19+ import org .springframework .test .web .servlet .MockMvc ;
20+
21+ import static org .fontory .fontorybe .TestConstants .*;
22+ import static org .mockito .ArgumentMatchers .any ;
23+ import static org .mockito .BDDMockito .given ;
24+ import static org .mockito .Mockito .verify ;
25+ import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .post ;
26+ import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .*;
27+
28+ /**
29+ * AuthController integration tests.
30+ * Tests authentication-related endpoints including logout functionality.
31+ */
32+ @ SpringBootTest
33+ @ AutoConfigureMockMvc
34+ @ Sql (value = "/sql/createMemberTestData.sql" , executionPhase = Sql .ExecutionPhase .BEFORE_TEST_METHOD )
35+ @ Sql (value = "/sql/deleteMemberTestData.sql" , executionPhase = Sql .ExecutionPhase .AFTER_TEST_METHOD )
36+ class AuthControllerIntegrationTest {
37+
38+ @ Autowired
39+ private MockMvc mockMvc ;
40+
41+ @ Autowired
42+ private ObjectMapper objectMapper ;
43+
44+ @ Autowired
45+ private JwtTokenProvider jwtTokenProvider ;
46+
47+ @ Autowired
48+ private MemberLookupService memberLookupService ;
49+
50+ @ MockitoBean
51+ private TokenStorage tokenStorage ;
52+
53+ private String validAccessToken ;
54+ private String validRefreshToken ;
55+ private Member testMember ;
56+ private UserPrincipal userPrincipal ;
57+
58+ @ BeforeEach
59+ void setUp () {
60+ testMember = memberLookupService .getOrThrowById (TEST_MEMBER_ID );
61+ userPrincipal = UserPrincipal .from (testMember );
62+
63+ validAccessToken = jwtTokenProvider .generateAccessToken (userPrincipal );
64+ validRefreshToken = jwtTokenProvider .generateRefreshToken (userPrincipal );
65+
66+ // Mock token storage behavior
67+ given (tokenStorage .getRefreshToken (any (Member .class )))
68+ .willReturn (validRefreshToken );
69+ }
70+
71+ @ Test
72+ @ DisplayName ("POST /auth/logout - successful logout with valid access token" )
73+ void testLogoutSuccess () throws Exception {
74+ // Given: Valid access token in cookie
75+ Cookie accessTokenCookie = new Cookie ("accessToken" , validAccessToken );
76+
77+ // When: Performing logout request
78+ mockMvc .perform (post ("/auth/logout" )
79+ .cookie (accessTokenCookie ))
80+ // Then: Should return 204 No Content
81+ .andExpect (status ().isNoContent ());
82+
83+ // Verify that token storage remove method was called
84+ verify (tokenStorage ).removeRefreshToken (any (Member .class ));
85+ }
86+
87+ @ Test
88+ @ DisplayName ("POST /auth/logout - logout without authentication returns 401" )
89+ void testLogoutWithoutAuthentication () throws Exception {
90+ // When: Performing logout request without access token
91+ mockMvc .perform (post ("/auth/logout" ))
92+ // Then: Should return 401 Unauthorized
93+ .andExpect (status ().isUnauthorized ())
94+ .andExpect (content ().contentType ("application/json;charset=UTF-8" ))
95+ .andExpect (jsonPath ("$.errorMessage" ).value ("Authentication Required." ));
96+ }
97+
98+ @ Test
99+ @ DisplayName ("POST /auth/logout - logout with invalid access token returns 401" )
100+ void testLogoutWithInvalidAccessToken () throws Exception {
101+ // Given: Invalid access token
102+ String invalidToken = "invalid.jwt.token" ;
103+ Cookie accessTokenCookie = new Cookie ("accessToken" , invalidToken );
104+
105+ // When: Performing logout request with invalid token
106+ mockMvc .perform (post ("/auth/logout" )
107+ .cookie (accessTokenCookie ))
108+ // Then: Should return 401 Unauthorized
109+ .andExpect (status ().isUnauthorized ())
110+ .andExpect (content ().contentType ("application/json;charset=UTF-8" ))
111+ .andExpect (jsonPath ("$.errorMessage" ).value ("Invalid access token" ));
112+ }
113+
114+ @ Test
115+ @ DisplayName ("POST /auth/logout - logout with expired access token returns 401" )
116+ void testLogoutWithExpiredAccessToken () throws Exception {
117+ // Given: Expired access token (this is complex to simulate)
118+ // For this test, we'll use a malformed token that will fail validation
119+ String expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.expired" ;
120+ Cookie accessTokenCookie = new Cookie ("accessToken" , expiredToken );
121+
122+ // When: Performing logout request with expired token
123+ mockMvc .perform (post ("/auth/logout" )
124+ .cookie (accessTokenCookie ))
125+ // Then: Should return 401 Unauthorized
126+ .andExpect (status ().isUnauthorized ())
127+ .andExpect (content ().contentType ("application/json;charset=UTF-8" ))
128+ .andExpect (jsonPath ("$.errorMessage" ).value ("Invalid access token" ));
129+ }
130+
131+ @ Test
132+ @ DisplayName ("POST /auth/logout - logout clears authentication cookies" )
133+ void testLogoutClearsAuthCookies () throws Exception {
134+ // Given: Valid access token and refresh token
135+ Cookie accessTokenCookie = new Cookie ("accessToken" , validAccessToken );
136+ Cookie refreshTokenCookie = new Cookie ("refreshToken" , validRefreshToken );
137+
138+ // When: Performing logout request
139+ mockMvc .perform (post ("/auth/logout" )
140+ .cookie (accessTokenCookie )
141+ .cookie (refreshTokenCookie ))
142+ // Then: Should return 204 and clear cookies
143+ .andExpect (status ().isNoContent ())
144+ // Verify that Set-Cookie headers are present to clear cookies
145+ .andExpect (header ().exists ("Set-Cookie" ));
146+
147+ // Verify that refresh token was removed from storage
148+ verify (tokenStorage ).removeRefreshToken (any (Member .class ));
149+ }
150+
151+ @ Test
152+ @ DisplayName ("POST /auth/logout - logout removes refresh token from storage" )
153+ void testLogoutRemovesRefreshTokenFromStorage () throws Exception {
154+ // Given: Valid access token
155+ Cookie accessTokenCookie = new Cookie ("accessToken" , validAccessToken );
156+
157+ // When: Performing logout request
158+ mockMvc .perform (post ("/auth/logout" )
159+ .cookie (accessTokenCookie ))
160+ .andExpect (status ().isNoContent ());
161+
162+ // Then: Verify that refresh token was removed from Redis storage
163+ verify (tokenStorage ).removeRefreshToken (any (Member .class ));
164+ }
165+
166+ @ Test
167+ @ DisplayName ("POST /auth/logout - logout with non-existent member returns appropriate error" )
168+ void testLogoutWithNonExistentMember () throws Exception {
169+ // Given: Valid JWT token for non-existent member
170+ UserPrincipal nonExistentUserPrincipal = new UserPrincipal (NON_EXIST_ID );
171+ String tokenForNonExistentUser = jwtTokenProvider .generateAccessToken (nonExistentUserPrincipal );
172+ Cookie accessTokenCookie = new Cookie ("accessToken" , tokenForNonExistentUser );
173+
174+ // When: Performing logout request
175+ mockMvc .perform (post ("/auth/logout" )
176+ .cookie (accessTokenCookie ))
177+ // Then: Should return error due to member not found
178+ .andExpect (status ().is4xxClientError ());
179+ }
180+
181+ @ Test
182+ @ DisplayName ("POST /auth/logout - multiple logout requests are idempotent" )
183+ void testMultipleLogoutRequestsAreIdempotent () throws Exception {
184+ // Given: Valid access token
185+ Cookie accessTokenCookie = new Cookie ("accessToken" , validAccessToken );
186+
187+ // When: Performing first logout request
188+ mockMvc .perform (post ("/auth/logout" )
189+ .cookie (accessTokenCookie ))
190+ .andExpect (status ().isNoContent ());
191+
192+ // When: Performing second logout request
193+ // Note: In an integration test, the same token can still be validated
194+ // since we're not actually clearing it from the JWT validation
195+ // but we are clearing it from Redis storage
196+ mockMvc .perform (post ("/auth/logout" )
197+ .cookie (accessTokenCookie ))
198+ // The request should still succeed as JWT validation passes
199+ .andExpect (status ().isNoContent ());
200+ }
201+
202+ @ Test
203+ @ DisplayName ("POST /auth/logout - only POST method is allowed" )
204+ void testLogoutOnlyAllowsPostMethod () throws Exception {
205+ // Given: Valid access token
206+ Cookie accessTokenCookie = new Cookie ("accessToken" , validAccessToken );
207+
208+ // When: Attempting GET request to logout endpoint
209+ mockMvc .perform (org .springframework .test .web .servlet .request .MockMvcRequestBuilders .get ("/auth/logout" )
210+ .cookie (accessTokenCookie ))
211+ // Then: Should return 405 Method Not Allowed
212+ .andExpect (status ().isMethodNotAllowed ());
213+
214+ // When: Attempting DELETE request to logout endpoint
215+ mockMvc .perform (org .springframework .test .web .servlet .request .MockMvcRequestBuilders .delete ("/auth/logout" )
216+ .cookie (accessTokenCookie ))
217+ // Then: Should return 405 Method Not Allowed
218+ .andExpect (status ().isMethodNotAllowed ());
219+ }
220+
221+ @ Test
222+ @ DisplayName ("POST /auth/logout - handles malformed JWT token gracefully" )
223+ void testLogoutWithMalformedJwtToken () throws Exception {
224+ // Given: Malformed JWT token
225+ String malformedToken = "not.a.valid.jwt.token.at.all" ;
226+ Cookie accessTokenCookie = new Cookie ("accessToken" , malformedToken );
227+
228+ // When: Performing logout request with malformed token
229+ mockMvc .perform (post ("/auth/logout" )
230+ .cookie (accessTokenCookie ))
231+ // Then: Should return 401 Unauthorized
232+ .andExpect (status ().isUnauthorized ())
233+ .andExpect (content ().contentType ("application/json;charset=UTF-8" ))
234+ .andExpect (jsonPath ("$.errorMessage" ).value ("Invalid access token" ));
235+ }
236+ }
0 commit comments