Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.nalssilog.common.exception;
package com.nalssilog.app.api;

import com.nalssilog.auth.config.AuthCookieManager;
import com.nalssilog.auth.domain.RefreshRejectedException;
import com.nalssilog.common.exception.ErrorCode;
import com.nalssilog.common.exception.ErrorResponse;
import com.nalssilog.common.exception.NalssiLogException;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -12,23 +18,32 @@

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

private final AuthCookieManager authCookieManager;

@ExceptionHandler(NalssiLogException.class)
public ResponseEntity<ErrorResponse> handleNalssiLogException(NalssiLogException e) {
ErrorCode errorCode = e.getErrorCode();
public ErrorResponse handleNalssiLogException(
NalssiLogException exception,
HttpServletResponse response
) {
if (exception instanceof RefreshRejectedException) {
authCookieManager.clearAuthCookies(response);
}

ErrorCode errorCode = exception.getErrorCode();
response.setStatus(errorCode.getStatus().value());
log.warn("NalssiLogException [{}] {} (status={})",
errorCode.getCode(), e.getMessage(), errorCode.getStatus().value());
errorCode.getCode(), exception.getMessage(), errorCode.getStatus().value());

return ResponseEntity.status(errorCode.getStatus())
.body(new ErrorResponse(errorCode.getCode(), e.getMessage()));
return new ErrorResponse(errorCode.getCode(), exception.getMessage());
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
public ErrorResponse handleValidationException(MethodArgumentNotValidException exception) {
String message = exception.getBindingResult().getFieldErrors().stream()
.findFirst()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.orElse("잘못된 요청입니다.");
Expand All @@ -38,20 +53,20 @@ public ErrorResponse handleValidationException(MethodArgumentNotValidException e

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public ErrorResponse handleNotReadable(HttpMessageNotReadableException e) {
public ErrorResponse handleNotReadable(HttpMessageNotReadableException exception) {
return new ErrorResponse("INVALID_REQUEST", "요청 본문을 해석할 수 없습니다.");
}

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoResourceFoundException.class)
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException e) {
public ErrorResponse handleNoResourceFoundException(NoResourceFoundException exception) {
return new ErrorResponse("NOT_FOUND", "요청한 리소스를 찾을 수 없습니다.");
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ErrorResponse handleUnexpectedException(Exception e) {
log.error("Unexpected exception", e);
public ErrorResponse handleUnexpectedException(Exception exception) {
log.error("Unexpected exception", exception);

return new ErrorResponse("INTERNAL_ERROR", "서버 오류가 발생했습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.nalssilog.app.api;

import static org.assertj.core.api.Assertions.assertThat;

import com.nalssilog.auth.config.AuthCookieManager;
import com.nalssilog.auth.config.AuthProperties;
import com.nalssilog.auth.domain.AuthErrorCode;
import com.nalssilog.auth.domain.RefreshRejectedException;
import com.nalssilog.common.exception.ErrorResponse;
import com.nalssilog.common.exception.NalssiLogException;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletResponse;

@SuppressWarnings("java:S5960")
class GlobalExceptionHandlerTest {

private final AuthCookieManager cookieManager = new AuthCookieManager(properties());
private final GlobalExceptionHandler exceptionHandler = new GlobalExceptionHandler(cookieManager);

@Test
void refreshRejectionDeletesBothAuthenticationCookies() {
RefreshRejectedException exception = new RefreshRejectedException(
new NalssiLogException(AuthErrorCode.AUTH_SESSION_EXPIRED));
MockHttpServletResponse response = new MockHttpServletResponse();

ErrorResponse errorResponse =
exceptionHandler.handleNalssiLogException(exception, response);

assertThat(response.getStatus()).isEqualTo(HttpStatus.UNAUTHORIZED.value());
assertThat(errorResponse).isEqualTo(new ErrorResponse(
AuthErrorCode.AUTH_SESSION_EXPIRED.getCode(),
AuthErrorCode.AUTH_SESSION_EXPIRED.getMessage()));
assertThat(response.getHeaders(HttpHeaders.SET_COOKIE))
.anySatisfy(header -> assertThat(header)
.contains("access_token=")
.contains("Path=/")
.contains("Max-Age=0")
.contains("Expires=Thu, 1 Jan 1970 00:00:00 GMT")
.contains("Secure")
.contains("HttpOnly")
.contains("SameSite=Lax")
.doesNotContain("Domain="))
.anySatisfy(header -> assertThat(header)
.contains("refresh_token=")
.contains("Path=/")
.contains("Max-Age=0")
.contains("Expires=Thu, 1 Jan 1970 00:00:00 GMT")
.contains("Secure")
.contains("HttpOnly")
.contains("SameSite=Lax")
.doesNotContain("Domain="));
}

private AuthProperties properties() {
return new AuthProperties(
new AuthProperties.Jwt(
"test-secret-must-be-at-least-thirty-two-bytes",
Duration.ofMinutes(30),
Duration.ofDays(14)),
new AuthProperties.Cookie(true),
new AuthProperties.Ticket(Duration.ofMinutes(10)),
new AuthProperties.Csrf("XSRF-TOKEN", null),
new AuthProperties.Refresh(Duration.ofSeconds(5)));
}
}
Loading