package com.baiyue.common.web; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.core.MethodParameter; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; import org.springframework.dao.CannotAcquireLockException; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import java.lang.reflect.Method; import static org.assertj.core.api.Assertions.assertThat; @DisplayName("全局异常响应") class GlobalExceptionHandlerTest { private final GlobalExceptionHandler handler = new GlobalExceptionHandler(); @Test void businessExceptionKeepsBusinessCodeAndCorrelation() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(CorrelationContext.REQUEST_ID, "r1"); request.setAttribute(CorrelationContext.TRACE_ID, "t1"); var response = handler.handleBusiness(new BizException("inventory is locked"), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.BUSINESS_ERROR.code()); assertThat(response.getBody().requestId()).isEqualTo("r1"); } @Test void invalidQueryParameterTypeMapsToBadRequest() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); Method method = GlobalExceptionHandlerTest.class.getDeclaredMethod("paging", long.class); MethodParameter parameter = new MethodParameter(method, 0); MethodArgumentTypeMismatchException exception = new MethodArgumentTypeMismatchException( "abc", Long.class, "page", parameter, new NumberFormatException("abc")); var response = handler.handleBadRequest(exception, request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code()); } @Test void missingRequiredRequestHeaderMapsToBadRequest() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest(); Method method = GlobalExceptionHandlerTest.class.getDeclaredMethod("paging", long.class); MethodParameter parameter = new MethodParameter(method, 0); var response = handler.handleBadRequest( new MissingRequestHeaderException("Idempotency-Key", parameter), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code()); } @Test void missingRequiredMultipartPartMapsToBadRequest() { MockHttpServletRequest request = new MockHttpServletRequest(); var response = handler.handleBadRequest( new MissingServletRequestPartException("file"), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code()); } @Test void validationDetailsAreIncludedInMessageAndData() { MockHttpServletRequest request = new MockHttpServletRequest(); BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(new Object(), "request"); bindingResult.addError(new FieldError("request", "carrierId", "不能为null")); bindingResult.addError(new FieldError("request", "carrierName", "不能为空")); var response = handler.handleBinding(new BindException(bindingResult), request); assertThat(response.getBody().code()).isEqualTo(ResultCode.VALIDATION_FAILED.code()); assertThat(response.getBody().message()).isEqualTo("carrierId: 不能为null;carrierName: 不能为空"); assertThat(response.getBody().data()).containsEntry("carrierId", "不能为null") .containsEntry("carrierName", "不能为空"); } @Test void recordsBusinessFailureMetricByFiniteResultCode() { SimpleMeterRegistry registry = new SimpleMeterRegistry(); ReflectionTestUtils.setField(handler, "meterRegistry", registry); handler.handleBusiness(new BizException(ResultCode.CONFLICT, "duplicate"), new MockHttpServletRequest()); assertThat(registry.get("baiyue.api.business.failures") .tag("code", ResultCode.CONFLICT.code()).counter().count()).isEqualTo(1.0); } @Test void responseStatusConflictMapsToCanonicalConflictInsteadOfInternalError() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(CorrelationContext.REQUEST_ID, "stale-version"); var response = handler.handleResponseStatus( new ResponseStatusException(HttpStatus.CONFLICT, "Order was changed; refresh and retry"), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.CONFLICT.code()); assertThat(response.getBody().message()).isEqualTo("Order was changed; refresh and retry"); assertThat(response.getBody().requestId()).isEqualTo("stale-version"); } @Test void missingControllerPathMapsToNotFoundInsteadOfInternalError() { MockHttpServletRequest request = new MockHttpServletRequest(); var response = handler.handleNoResourceFound( new NoResourceFoundException(org.springframework.http.HttpMethod.GET, "api/platform/v1/last-leg/dispatches"), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.NOT_FOUND.code()); } @Test void lockWaitTimeoutMapsToCanonicalConflict() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(CorrelationContext.REQUEST_ID, "lock-timeout"); var response = handler.handleCannotAcquireLock( new CannotAcquireLockException("Lock wait timeout exceeded"), request); assertThat(response.getBody().code()).isEqualTo(ResultCode.CONFLICT.code()); assertThat(response.getBody().requestId()).isEqualTo("lock-timeout"); } @Test void oversizedMultipartReturnsClearBadRequestInsteadOfInternalError() { MockHttpServletRequest request = new MockHttpServletRequest(); request.setAttribute(CorrelationContext.REQUEST_ID, "oversized-photo"); var response = handler.handleMaxUploadSize( new MaxUploadSizeExceededException(10L * 1024 * 1024), request); assertThat(response.getStatusCode().value()).isEqualTo(200); assertThat(response.getBody().code()).isEqualTo(ResultCode.BAD_REQUEST.code()); assertThat(response.getBody().message()).isEqualTo("上传文件不能超过10MB"); assertThat(response.getBody().requestId()).isEqualTo("oversized-photo"); } private void paging(long page) { } }