Skip to content

Commit b53cf6d

Browse files
authored
[fix] 로그아웃 프론트 500에러 수정 (#225)
1 parent 1f146de commit b53cf6d

File tree

2 files changed

+128
-82
lines changed

2 files changed

+128
-82
lines changed

src/main/java/com/back/domain/auth/controller/AuthController.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
import org.springframework.http.ResponseCookie;
2323
import org.springframework.http.ResponseEntity;
2424
import org.springframework.security.core.Authentication;
25-
import org.springframework.web.bind.annotation.PostMapping;
26-
import org.springframework.web.bind.annotation.RequestBody;
27-
import org.springframework.web.bind.annotation.RequestMapping;
28-
import org.springframework.web.bind.annotation.RestController;
25+
import org.springframework.web.bind.annotation.*;
2926

3027
@Slf4j
3128
@RestController
@@ -107,10 +104,19 @@ public ResponseEntity<RsData<AuthResponse>> login(
107104
@PostMapping("/logout")
108105
@Operation(summary = "로그아웃", description = "현재 기기에서 로그아웃을 처리합니다.")
109106
public ResponseEntity<RsData<Void>> logout(
110-
@Valid @RequestBody TokenRefreshRequest request
107+
@CookieValue(value = "refreshToken", required = false) String refreshToken
111108
) {
112-
authService.logout(request.refreshToken());
113109

110+
// RefreshToken이 있으면 DB에서 삭제
111+
if (refreshToken != null && !refreshToken.isBlank()) {
112+
try {
113+
authService.logout(refreshToken);
114+
} catch (Exception e) {
115+
log.warn("로그아웃 처리 중 오류 발생 (쿠키는 삭제됨): {}", e.getMessage());
116+
}
117+
}
118+
119+
// 쿠키 삭제 (토큰 유무와 관계없이 실행)
114120
HttpHeaders headers = new HttpHeaders();
115121
headers.add(HttpHeaders.SET_COOKIE, deleteCookie("refreshToken").toString());
116122
headers.add(HttpHeaders.SET_COOKIE, deleteCookie("accessToken").toString());

src/test/java/com/back/domain/auth/controller/AuthControllerTest.java

Lines changed: 116 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
package com.back.domain.auth.controller;
22

3-
import com.back.domain.auth.controller.AuthController;
4-
import com.back.domain.auth.dto.request.TokenRefreshRequest;
5-
import com.back.domain.auth.dto.response.AuthResponse;
6-
import com.back.domain.user.entity.Role;
7-
import com.back.global.exception.ServiceException;
8-
import com.back.global.rsData.RsData;
9-
import org.junit.jupiter.api.DisplayName;
10-
import org.junit.jupiter.api.Test;
11-
import org.springframework.http.ResponseEntity;
12-
13-
import java.util.List;
14-
153
import com.back.domain.auth.dto.request.LoginRequest;
164
import com.back.domain.auth.dto.request.SignUpRequest;
175
import com.back.domain.auth.dto.request.TokenRefreshRequest;
186
import com.back.domain.auth.dto.response.AuthResponse;
197
import com.back.domain.auth.dto.response.SignUpResponse;
208
import com.back.domain.auth.service.AuthService;
219
import com.back.domain.user.entity.Role;
10+
import com.back.global.exception.ServiceException;
2211
import com.back.global.rsData.RsData;
2312
import com.back.global.security.auth.CustomUserDetails;
2413
import jakarta.servlet.http.HttpServletRequest;
@@ -357,145 +346,196 @@ void login_TokenExpirationIncluded() {
357346
class LogoutTest {
358347

359348
@Test
360-
@DisplayName("정상적인 로그아웃 요청 성공")
361-
void logout_Success() {
349+
@DisplayName("쿠키에 RefreshToken이 있는 경우 정상 로그아웃")
350+
void logout_WithRefreshTokenCookie_Success() {
362351
// given
363-
TokenRefreshRequest request = new TokenRefreshRequest("validRefreshToken");
352+
String refreshToken = "validRefreshToken";
364353

365354
// when
366-
ResponseEntity<RsData<Void>> response = authController.logout(request);
355+
ResponseEntity<RsData<Void>> response = authController.logout(refreshToken);
367356

368357
// then
369358
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
370359
assertThat(response.getBody().resultCode()).isEqualTo("200");
371360
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
372361
assertThat(response.getBody().data()).isNull();
362+
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
373363

374364
verify(authService).logout("validRefreshToken");
375365
}
376366

377367
@Test
378-
@DisplayName("다양한 형태의 RefreshToken으로 로그아웃 처리")
379-
void logout_DifferentTokenFormats() {
368+
@DisplayName("쿠키에 RefreshToken이 없는 경우에도 로그아웃 성공 (쿠키만 삭제)")
369+
void logout_WithoutRefreshTokenCookie_Success() {
370+
// given - RefreshToken이 null인 경우
371+
372+
// when
373+
ResponseEntity<RsData<Void>> response = authController.logout(null);
374+
375+
// then
376+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
377+
assertThat(response.getBody().resultCode()).isEqualTo("200");
378+
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
379+
assertThat(response.getBody().data()).isNull();
380+
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
381+
382+
// RefreshToken이 없으면 authService.logout()이 호출되지 않음
383+
verify(authService, never()).logout(any());
384+
}
385+
386+
@Test
387+
@DisplayName("빈 문자열 RefreshToken인 경우 로그아웃 (쿠키만 삭제)")
388+
void logout_WithEmptyRefreshToken_Success() {
380389
// given
381-
String[] tokens = {
390+
String emptyRefreshToken = "";
391+
392+
// when
393+
ResponseEntity<RsData<Void>> response = authController.logout(emptyRefreshToken);
394+
395+
// then
396+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
397+
assertThat(response.getBody().resultCode()).isEqualTo("200");
398+
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
399+
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
400+
401+
// 빈 문자열이면 authService.logout()이 호출되지 않음
402+
verify(authService, never()).logout(any());
403+
}
404+
405+
@Test
406+
@DisplayName("공백 문자열 RefreshToken인 경우 로그아웃 (쿠키만 삭제)")
407+
void logout_WithBlankRefreshToken_Success() {
408+
// given
409+
String blankRefreshToken = " ";
410+
411+
// when
412+
ResponseEntity<RsData<Void>> response = authController.logout(blankRefreshToken);
413+
414+
// then
415+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
416+
assertThat(response.getBody().resultCode()).isEqualTo("200");
417+
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
418+
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
419+
420+
// 공백 문자열이면 authService.logout()이 호출되지 않음
421+
verify(authService, never()).logout(any());
422+
}
423+
424+
@Test
425+
@DisplayName("다양한 형태의 유효한 RefreshToken으로 로그아웃")
426+
void logout_WithVariousValidTokens_Success() {
427+
// given
428+
String[] validTokens = {
382429
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWI.signature",
383430
"shortToken123",
384431
"veryLongTokenWith1234567890AbcdefghijklmnopqrstuvwxyzMore",
385432
"token-with-dashes-and-underscores_123"
386433
};
387434

388-
for (String token : tokens) {
389-
TokenRefreshRequest request = new TokenRefreshRequest(token);
390-
435+
for (String token : validTokens) {
391436
// when
392-
ResponseEntity<RsData<Void>> response = authController.logout(request);
437+
ResponseEntity<RsData<Void>> response = authController.logout(token);
393438

394439
// then
395440
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
396441
verify(authService).logout(token);
397442
}
398443

399-
verify(authService, times(tokens.length)).logout(anyString());
444+
verify(authService, times(validTokens.length)).logout(anyString());
400445
}
401446

402447
@Test
403448
@DisplayName("로그아웃 응답에 쿠키 삭제 헤더가 포함되는지 확인")
404-
void logout_ResponseContainsCookieDeletion() {
449+
void logout_ResponseContainsCookieDeletionHeaders() {
405450
// given
406-
TokenRefreshRequest request = new TokenRefreshRequest("validRefreshToken");
451+
String refreshToken = "validRefreshToken";
407452

408453
// when
409-
ResponseEntity<RsData<Void>> response = authController.logout(request);
454+
ResponseEntity<RsData<Void>> response = authController.logout(refreshToken);
410455

411456
// then
412457
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
413458
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
414-
// Set-Cookie 헤더로 쿠키 삭제(MaxAge=0) 헤더가 추가됨을 확인
459+
assertThat(response.getHeaders().get("Set-Cookie").size()).isEqualTo(2);
460+
// refreshToken과 accessToken 쿠키 삭제 헤더 2개 포함됨
415461
}
416462

417463
@Test
418-
@DisplayName("빈 문자열 토큰으로 로그아웃 시도")
419-
void logout_EmptyToken() {
464+
@DisplayName("동일한 RefreshToken으로 연속 로그아웃 요청")
465+
void logout_ConsecutiveRequestsWithSameToken() {
420466
// given
421-
TokenRefreshRequest request = new TokenRefreshRequest("");
467+
String refreshToken = "sameToken";
422468

423469
// when
424-
ResponseEntity<RsData<Void>> response = authController.logout(request);
470+
ResponseEntity<RsData<Void>> response1 = authController.logout(refreshToken);
471+
ResponseEntity<RsData<Void>> response2 = authController.logout(refreshToken);
425472

426473
// then
427-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
428-
// Controller는 빈 문자열도 그대로 Service에 전달
429-
verify(authService).logout("");
474+
assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK);
475+
assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK);
476+
verify(authService, times(2)).logout("sameToken");
430477
}
431478

432479
@Test
433-
@DisplayName("공백으로만 이루어진 토큰으로 로그아웃 시도")
434-
void logout_WhitespaceToken() {
480+
@DisplayName("여러 사용자의 동시 로그아웃 요청 처리")
481+
void logout_MultipleConcurrentUsers() {
435482
// given
436-
TokenRefreshRequest request = new TokenRefreshRequest(" ");
483+
String[] userTokens = {"userToken1", "userToken2", "userToken3"};
437484

438-
// when
439-
ResponseEntity<RsData<Void>> response = authController.logout(request);
485+
for (String token : userTokens) {
486+
// when
487+
ResponseEntity<RsData<Void>> response = authController.logout(token);
440488

441-
// then
442-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
443-
verify(authService).logout(" ");
489+
// then
490+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
491+
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
492+
verify(authService).logout(token);
493+
}
494+
495+
verify(authService, times(userTokens.length)).logout(anyString());
444496
}
445497

446498
@Test
447-
@DisplayName("연속 로그아웃 요청 처리")
448-
void logout_MultipleLogoutRequests() {
499+
@DisplayName("로그아웃 후 응답 데이터가 null인지 확인")
500+
void logout_ResponseDataIsNull() {
449501
// given
450-
TokenRefreshRequest request1 = new TokenRefreshRequest("token1");
451-
TokenRefreshRequest request2 = new TokenRefreshRequest("token2");
452-
TokenRefreshRequest request3 = new TokenRefreshRequest("token3");
502+
String refreshToken = "validRefreshToken";
453503

454504
// when
455-
ResponseEntity<RsData<Void>> response1 = authController.logout(request1);
456-
ResponseEntity<RsData<Void>> response2 = authController.logout(request2);
457-
ResponseEntity<RsData<Void>> response3 = authController.logout(request3);
505+
ResponseEntity<RsData<Void>> response = authController.logout(refreshToken);
458506

459507
// then
460-
assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK);
461-
assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK);
462-
assertThat(response3.getStatusCode()).isEqualTo(HttpStatus.OK);
463-
464-
verify(authService).logout("token1");
465-
verify(authService).logout("token2");
466-
verify(authService).logout("token3");
467-
verify(authService, times(3)).logout(anyString());
508+
assertThat(response.getBody().data()).isNull();
509+
// Void 타입이므로 data는 항상 null
468510
}
469511

470512
@Test
471-
@DisplayName("동일한 토큰으로 중복 로그아웃 요청")
472-
void logout_DuplicateLogoutRequests() {
513+
@DisplayName("만료된 RefreshToken으로도 쿠키 삭제는 정상 수행")
514+
void logout_WithExpiredToken_StillDeletesCookies() {
473515
// given
474-
TokenRefreshRequest request = new TokenRefreshRequest("sameToken");
516+
String expiredToken = "expiredRefreshToken";
517+
// Service에서 만료된 토큰 처리 시 예외가 발생할 수 있지만
518+
// Controller는 쿠키 삭제를 정상 수행해야 함
475519

476520
// when
477-
ResponseEntity<RsData<Void>> response1 = authController.logout(request);
478-
ResponseEntity<RsData<Void>> response2 = authController.logout(request);
521+
ResponseEntity<RsData<Void>> response = authController.logout(expiredToken);
479522

480523
// then
481-
assertThat(response1.getStatusCode()).isEqualTo(HttpStatus.OK);
482-
assertThat(response2.getStatusCode()).isEqualTo(HttpStatus.OK);
483-
// Controller는 중복 요청도 동일하게 처리
484-
verify(authService, times(2)).logout("sameToken");
524+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
525+
assertThat(response.getHeaders().get("Set-Cookie")).isNotNull();
526+
// 쿠키 삭제는 토큰 유효성과 관계없이 항상 수행됨
485527
}
486528

487529
@Test
488-
@DisplayName("로그아웃 후 응답 데이터가 null인지 확인")
489-
void logout_ResponseDataIsNull() {
490-
// given
491-
TokenRefreshRequest request = new TokenRefreshRequest("validRefreshToken");
492-
530+
@DisplayName("RefreshToken이 null이어도 HTTP 200 응답")
531+
void logout_NullToken_Returns200() {
493532
// when
494-
ResponseEntity<RsData<Void>> response = authController.logout(request);
533+
ResponseEntity<RsData<Void>> response = authController.logout(null);
495534

496535
// then
497-
assertThat(response.getBody().data()).isNull();
498-
// Void 타입이므로 data는 항상 null이어야 함
536+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
537+
assertThat(response.getBody().resultCode()).isEqualTo("200");
538+
assertThat(response.getBody().msg()).isEqualTo("로그아웃 성공");
499539
}
500540
}
501541

0 commit comments

Comments
 (0)